From 76706144a570d04313edc2b7216875a4ee1e3a7f Mon Sep 17 00:00:00 2001 From: Dylan Dmitri Gray Date: Wed, 1 Aug 2018 09:31:43 -0700 Subject: [PATCH] Add support for streaming arguments to clients using Channel (#2441) --- NuGet.config | 2 +- .../TestBinder.cs | 5 + clients/ts/FunctionalTests/package-lock.json | 254 +++++++++--------- samples/ClientSample/Program.cs | 2 + samples/ClientSample/StreamingSample.cs | 46 ++++ samples/ClientSample/UploadSample.cs | 90 +++++++ samples/SignalRSamples/Hubs/UploadHub.cs | 110 ++++++++ samples/SignalRSamples/Startup.cs | 1 + src/Common/ReflectionHelper.cs | 39 +++ .../HubConnection.Log.cs | 32 +++ .../HubConnection.cs | 155 +++++++++-- ...soft.AspNetCore.SignalR.Client.Core.csproj | 1 + .../IInvocationBinder.cs | 1 + .../Protocol/HubProtocolConstants.cs | 10 + .../Protocol/StreamBindingFailureMessage.cs | 37 +++ .../Protocol/StreamCompleteMessage.cs | 41 +++ .../Protocol/StreamDataMessage.cs | 33 +++ .../Protocol/StreamPlaceholder.cs | 25 ++ .../breakingchanges.netcore.json | 5 + .../HubConnectionContext.cs | 13 + .../HubConnectionHandler.cs | 5 +- .../Internal/DefaultHubDispatcher.Log.cs | 32 +++ .../Internal/DefaultHubDispatcher.cs | 131 +++++++-- .../Internal/HubConnectionBinder.cs | 36 +++ .../Internal/HubDispatcher.cs | 5 +- .../Internal/HubMethodDescriptor.cs | 16 +- .../Microsoft.AspNetCore.SignalR.Core.csproj | 4 + .../StreamTracker.cs | 105 ++++++++ ...t.AspNetCore.SignalR.Protocols.Json.csproj | 2 +- .../Protocol/JsonHubProtocol.cs | 115 +++++++- .../Protocol/MessagePackHubProtocol.cs | 36 ++- .../HubConnectionTests.cs | 224 +++++++++++++++ .../TestConnection.cs | 5 + .../Internal/Protocol/CompositeTestBinder.cs | 5 + .../Internal/Protocol/JsonHubProtocolTests.cs | 10 +- .../Protocol/MessagePackHubProtocolTests.cs | 24 +- .../Internal/Protocol/TestBinder.cs | 8 + .../TestHubMessageEqualityComparer.cs | 59 ++-- .../TestClient.cs | 12 + .../HubConnectionHandlerTestUtils/Hubs.cs | 73 +++++ .../HubConnectionHandlerTests.cs | 210 ++++++++++++++- 41 files changed, 1797 insertions(+), 222 deletions(-) create mode 100644 samples/ClientSample/StreamingSample.cs create mode 100644 samples/ClientSample/UploadSample.cs create mode 100644 samples/SignalRSamples/Hubs/UploadHub.cs create mode 100644 src/Common/ReflectionHelper.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs create mode 100644 src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs diff --git a/NuGet.config b/NuGet.config index e32bddfd51..1280d2db38 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,6 +2,6 @@ - + diff --git a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs index 1e25041d4f..bbcb8a1bde 100644 --- a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs +++ b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs @@ -59,5 +59,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks } throw new InvalidOperationException("Unexpected binder call"); } + + public Type GetStreamItemType(string streamId) + { + throw new NotImplementedException(); + } } } diff --git a/clients/ts/FunctionalTests/package-lock.json b/clients/ts/FunctionalTests/package-lock.json index 2a949153c3..9dd1fd7506 100644 --- a/clients/ts/FunctionalTests/package-lock.json +++ b/clients/ts/FunctionalTests/package-lock.json @@ -183,7 +183,7 @@ "adm-zip": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz", - "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==", + "integrity": "sha1-KqVMhMSwGp0PuJuxGYKlHxPj1io=", "dev": true }, "after": { @@ -216,7 +216,7 @@ "amqplib": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.5.2.tgz", - "integrity": "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA==", + "integrity": "sha1-0tcxPH/6pNELzx5iUt5FkbbMe2M=", "dev": true, "optional": true, "requires": { @@ -274,7 +274,7 @@ "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "integrity": "sha1-vLJLTzeTTZqnrBe0ra+J58du8us=", "dev": true, "requires": { "micromatch": "^3.1.4", @@ -320,7 +320,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", "dev": true }, "arr-union": { @@ -344,7 +344,7 @@ "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=", "dev": true }, "arrify": { @@ -374,14 +374,14 @@ "ast-types": { "version": "0.11.5", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.5.tgz", - "integrity": "sha512-oJjo+5e7/vEc2FBK8gUalV0pba4L3VdBIs2EKhOLHLcOd2FgQIVQN9xb0eZ9IjEWyAL7vq6fGJxOvVvdCHNyMw==", + "integrity": "sha1-mJCCXWYMA8KDOfMV6foKNg4x7Cg=", "dev": true, "optional": true }, "async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "integrity": "sha1-skWiPKcZMAROxT+kaqAKPofGphA=", "dev": true, "requires": { "lodash": "^4.17.10" @@ -396,7 +396,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=", "dev": true }, "asynckit": { @@ -420,7 +420,7 @@ "aws4": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "integrity": "sha1-1NDpudv8p3vwjusKikcVUP454ok=", "dev": true }, "axios": { @@ -470,7 +470,7 @@ "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", "dev": true, "requires": { "cache-base": "^1.0.1", @@ -494,7 +494,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -503,7 +503,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -512,7 +512,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -587,7 +587,7 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=", "dev": true }, "body-parser": { @@ -611,7 +611,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "dev": true, "requires": { "ms": "2.0.0" @@ -641,7 +641,7 @@ "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", "dev": true, "requires": { "arr-flatten": "^1.1.0", @@ -670,7 +670,7 @@ "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=", "dev": true, "requires": { "buffer-alloc-unsafe": "^1.1.0", @@ -680,7 +680,7 @@ "buffer-alloc-unsafe": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA=", "dev": true }, "buffer-crc32": { @@ -732,7 +732,7 @@ "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", "dev": true, "requires": { "collection-visit": "^1.0.0", @@ -772,7 +772,7 @@ "chokidar": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "integrity": "sha1-NW/04rDo5D4yLRijckYLvPOszSY=", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -799,7 +799,7 @@ "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", "dev": true, "requires": { "arr-union": "^3.1.0", @@ -853,7 +853,7 @@ "colors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==", + "integrity": "sha1-XyDJ/vaUXLETQmCqszv73IKV4E4=", "dev": true }, "combine-lists": { @@ -943,7 +943,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "cookie": { @@ -961,7 +961,7 @@ "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "integrity": "sha1-+XJgj/DOrWi4QaFqky0LGDeRgU4=", "dev": true }, "core-util-is": { @@ -1013,7 +1013,7 @@ "data-uri-to-buffer": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", - "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==", + "integrity": "sha1-dxY+qcINhkG0cH6PGKvfmnjzSDU=", "dev": true, "optional": true }, @@ -1048,7 +1048,7 @@ "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", "dev": true, "requires": { "is-descriptor": "^1.0.2", @@ -1058,7 +1058,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -1067,7 +1067,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -1076,7 +1076,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -1172,7 +1172,7 @@ "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", "dev": true, "requires": { "once": "^1.4.0" @@ -1181,7 +1181,7 @@ "engine.io": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.5.tgz", - "integrity": "sha512-D06ivJkYxyRrcEe0bTpNnBQNgP9d3xog+qZlLbui8EsMr/DouQpf5o9FzJnWYHEYE0YsFHllUv2R1dkgYZXHcA==", + "integrity": "sha1-Dn751pDrCzVZfx1K0Comyi26OEU=", "dev": true, "requires": { "accepts": "~1.3.4", @@ -1196,7 +1196,7 @@ "engine.io-client": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.6.tgz", - "integrity": "sha512-hnuHsFluXnsKOndS4Hv6SvUrgdYx1pk2NqfaDMW+GWdgfU3+/V25Cj7I8a0x92idSpa5PIhJRKxPvp9mnoLsfg==", + "integrity": "sha1-W96xMPi5SlCsXL63JYPnpKBj3f0=", "dev": true, "requires": { "component-emitter": "1.2.1", @@ -1215,7 +1215,7 @@ "engine.io-parser": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", - "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", + "integrity": "sha1-TA9M/3mq7su9z96maoI8YIVAkZY=", "dev": true, "requires": { "after": "0.8.2", @@ -1261,7 +1261,7 @@ "escodegen": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz", - "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==", + "integrity": "sha1-9kc5XeIlGfvQ2Sj/zx0X4N7CYD4=", "dev": true, "optional": true, "requires": { @@ -1295,7 +1295,7 @@ "eventemitter3": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "integrity": "sha1-CQtNbNvWRe0Qv3UNS1QHlC17oWM=", "dev": true }, "expand-braces": { @@ -1413,7 +1413,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -1424,7 +1424,7 @@ "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", "dev": true, "requires": { "array-unique": "^0.3.2", @@ -1458,7 +1458,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -1467,7 +1467,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -1476,7 +1476,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -1514,7 +1514,7 @@ "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "integrity": "sha1-VTp7hEb/b2hDWcRF8eN6BdrMM90=", "dev": true, "optional": true }, @@ -1626,7 +1626,7 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", "dev": true }, "fs.realpath": { @@ -2224,7 +2224,7 @@ "get-uri": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.2.tgz", - "integrity": "sha512-ZD325dMZOgerGqF/rF6vZXyFGTAay62svjQIT+X/oU2PtxYpFxvSkbsdi+oxIrsNxlZVd4y8wUDqkaExWTI/Cw==", + "integrity": "sha1-XHlecTJvbKEoby/IJXXNK6sq9Xg=", "dev": true, "optional": true, "requires": { @@ -2333,7 +2333,7 @@ "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", "dev": true, "requires": { "isarray": "2.0.1" @@ -2445,7 +2445,7 @@ "http-proxy": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "integrity": "sha1-etOElGWPhGBeL220Q230EPTlvpo=", "dev": true, "requires": { "eventemitter3": "^3.0.0", @@ -2456,7 +2456,7 @@ "http-proxy-agent": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "integrity": "sha1-5IIb7vWyFCogJr1zkm/lN2McVAU=", "dev": true, "requires": { "agent-base": "4", @@ -2493,7 +2493,7 @@ "https-proxy-agent": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "integrity": "sha1-UVUpcPoE1yPgTFbQQXjD+SWSu8A=", "dev": true, "requires": { "agent-base": "^4.1.0", @@ -2503,7 +2503,7 @@ "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=", "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -2575,7 +2575,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", "dev": true }, "is-data-descriptor": { @@ -2601,7 +2601,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", @@ -2612,7 +2612,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", "dev": true } } @@ -2641,14 +2641,14 @@ "is-my-ip-valid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "integrity": "sha1-ezUbjo7dTTmV1NBmaA5mTZRpaCQ=", "dev": true, "optional": true }, "is-my-json-valid": { "version": "2.17.2", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "integrity": "sha1-ayEDoojpTvPeXPFdKd2F/Et41lw=", "dev": true, "optional": true, "requires": { @@ -2682,7 +2682,7 @@ "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "dev": true, "requires": { "isobject": "^3.0.1" @@ -2711,7 +2711,7 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", "dev": true }, "isarray": { @@ -2806,7 +2806,7 @@ "karma": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/karma/-/karma-2.0.4.tgz", - "integrity": "sha512-32yhTwoi6BZgJZhR78GwhzyFABbYG/1WwQqYgY7Vh96Demvua2jM3+FyRltIMTUH/Kd5xaQvDw2L7jTvkYFeXg==", + "integrity": "sha1-s5l4X1fpurHTxDhNsz/vTeyK40k=", "dev": true, "requires": { "bluebird": "^3.3.0", @@ -2841,7 +2841,7 @@ "karma-chrome-launcher": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", - "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "integrity": "sha1-zxudBxNswY/iOTJ9JGVMPbw2is8=", "dev": true, "requires": { "fs-access": "^1.0.0", @@ -2851,7 +2851,7 @@ "karma-edge-launcher": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", - "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", + "integrity": "sha1-PZUpsJsTyQnF887uEtAOf5qYmz0=", "dev": true, "requires": { "edge-launcher": "1.2.2" @@ -2860,7 +2860,7 @@ "karma-firefox-launcher": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", - "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==", + "integrity": "sha1-LEcDBFLwRTHrfRPU/HZpYwu5Mzk=", "dev": true }, "karma-ie-launcher": { @@ -2898,7 +2898,7 @@ "karma-sauce-launcher": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/karma-sauce-launcher/-/karma-sauce-launcher-1.2.0.tgz", - "integrity": "sha512-lEhtGRGS+3Yw6JSx/vJY9iQyHNtTjcojrSwNzqNUOaDceKDu9dPZqA/kr69bUO9G2T6GKbu8AZgXqy94qo31Jg==", + "integrity": "sha1-byVY3e889Wh5+idUDIrp+L/Ra8o=", "dev": true, "requires": { "q": "^1.5.0", @@ -2928,7 +2928,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true }, "lazystream": { @@ -2985,7 +2985,7 @@ "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "integrity": "sha1-G3eTz3JZ6jj7NmHU04syYK+K5Oc=", "dev": true }, "lodash.debounce": { @@ -2997,7 +2997,7 @@ "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=", "dev": true, "requires": { "chalk": "^2.0.1" @@ -3006,7 +3006,7 @@ "log4js": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/log4js/-/log4js-2.10.0.tgz", - "integrity": "sha512-NnhN9PjFF9zhxinAjlmDYvkqqrIW+yA3LLJAoTJ3fs6d1zru86OqQHfsxiUcc1kRq3z+faGR4DeyXUfiNbVxKQ==", + "integrity": "sha1-SYC9kUigMNafnt3jKlsZwKVR4sQ=", "dev": true, "requires": { "amqplib": "^0.5.2", @@ -3227,7 +3227,7 @@ "lru-cache": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "integrity": "sha1-oRdc80lt/IQ2wVbDNLSVWZK85pw=", "dev": true, "optional": true, "requires": { @@ -3249,7 +3249,7 @@ "mailgun-js": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.18.1.tgz", - "integrity": "sha512-lvuMP14u24HS2uBsJEnzSyPMxzU2b99tQsIx1o6QNjqxjk8b3WvR+vq5oG1mjqz/IBYo+5gF+uSoDS0RkMVHmg==", + "integrity": "sha1-7jmqGNe7WYpc6e3oSvtoHe/IprA=", "dev": true, "optional": true, "requires": { @@ -3294,7 +3294,7 @@ "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -3315,19 +3315,19 @@ "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", "dev": true }, "mime-db": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "integrity": "sha1-o0kgUKXLm2NFBUHjnZeI0icng9s=", "dev": true }, "mime-types": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "integrity": "sha1-bzI/YKg9ERRvgx/xH9ZuL+VQO7g=", "dev": true, "requires": { "mime-db": "~1.33.0" @@ -3351,7 +3351,7 @@ "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", "dev": true, "requires": { "for-in": "^1.0.2", @@ -3361,7 +3361,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -3395,7 +3395,7 @@ "msgpack5": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.2.0.tgz", - "integrity": "sha512-tQkRlwO4f3/E8Kq5qm6PcVw+J+K4+U/XNqeD9Ebo1qVsrjkcKb2FfmdtuuIslw42CGT+K3ZVKAvKfSPp3QRplQ==", + "integrity": "sha1-4AXsiTtx4RQLsXfy0fS38pAWlhE=", "requires": { "bl": "^2.0.0", "inherits": "^2.0.3", @@ -3406,7 +3406,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" } } }, @@ -3420,7 +3420,7 @@ "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -3676,7 +3676,7 @@ "pac-proxy-agent": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-2.0.2.tgz", - "integrity": "sha512-cDNAN1Ehjbf5EHkNY5qnRhGPUCp6SnpyVof5fRzN800QV1Y2OkzbH9rmjZkbBRa8igof903yOnjIl6z0SlAhxA==", + "integrity": "sha1-kNn2cwqw9NJgfc3NTT1kGqJsOJY=", "dev": true, "optional": true, "requires": { @@ -3693,7 +3693,7 @@ "pac-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", - "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "integrity": "sha1-auoweH2wqJFwTet4AKcip2FabyY=", "dev": true, "optional": true, "requires": { @@ -3809,7 +3809,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, "promisify-call": { "version": "2.0.4", @@ -3824,7 +3824,7 @@ "proxy-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.0.0.tgz", - "integrity": "sha512-g6n6vnk8fRf705ShN+FEXFG/SDJaW++lSs0d9KaJh4uBWW/wi7en4Cpo5VYQW3SZzAE121lhB/KLQrbURoubZw==", + "integrity": "sha1-9naOICiJsihdOZBtOpR2hBb49xM=", "dev": true, "optional": true, "requires": { @@ -3867,13 +3867,13 @@ "qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "integrity": "sha1-xF6cYYAL0IfviNfiVkI73Unl0HE=", "dev": true }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=", "dev": true }, "range-parser": { @@ -3885,7 +3885,7 @@ "raw-body": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "integrity": "sha1-GzJOzmtXBuFThVvBFIxlu39uoMM=", "dev": true, "requires": { "bytes": "3.0.0", @@ -3897,7 +3897,7 @@ "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3923,7 +3923,7 @@ "redis": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "integrity": "sha1-ICKI4/WMSfYHnZevehDhMDrhSwI=", "dev": true, "optional": true, "requires": { @@ -3935,7 +3935,7 @@ "redis-commands": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz", - "integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA==", + "integrity": "sha1-RJWIlBTx6IYmEYCxRC5ylWAtg6I=", "dev": true, "optional": true }, @@ -3949,7 +3949,7 @@ "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", "dev": true, "requires": { "extend-shallow": "^3.0.2", @@ -3977,7 +3977,7 @@ "request": { "version": "2.87.0", "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "integrity": "sha1-MvACNc0I1IK00NaNuTqCnA7VdW4=", "dev": true, "optional": true, "requires": { @@ -4006,7 +4006,7 @@ "requestretry": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/requestretry/-/requestretry-1.13.0.tgz", - "integrity": "sha512-Lmh9qMvnQXADGAQxsXHP4rbgO6pffCfuR8XUBdP9aitJcLQJxhp7YZK4xAVYXnPJ5E52mwrfiKQtKonPL8xsmg==", + "integrity": "sha1-IT7BAG7rdQ6LjOVBdig9FajVXZQ=", "dev": true, "optional": true, "requires": { @@ -4031,13 +4031,13 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", "dev": true }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", "dev": true, "requires": { "glob": "^7.0.5" @@ -4060,13 +4060,13 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", "dev": true }, "sauce-connect-launcher": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sauce-connect-launcher/-/sauce-connect-launcher-1.2.4.tgz", - "integrity": "sha512-X2vfwulR6brUGiicXKxPm1GJ7dBEeP1II450Uv4bHGrcGOapZNgzJvn9aioea5IC5BPp/7qjKdE3xbbTBIVXMA==", + "integrity": "sha1-jTj4UkKp++3hsjA7VZ9+IMVgmhw=", "dev": true, "requires": { "adm-zip": "~0.4.3", @@ -4079,7 +4079,7 @@ "saucelabs": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", - "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "integrity": "sha1-lAWnPDYNRJsjKDmRmobDltN5/Z0=", "dev": true, "requires": { "https-proxy-agent": "^2.2.1" @@ -4088,7 +4088,7 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=", "dev": true }, "set-immediate-shim": { @@ -4100,7 +4100,7 @@ "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -4123,7 +4123,7 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=", "dev": true }, "slack-node": { @@ -4155,7 +4155,7 @@ "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", "dev": true, "requires": { "base": "^0.11.1", @@ -4206,7 +4206,7 @@ "snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", "dev": true, "requires": { "define-property": "^1.0.0", @@ -4226,7 +4226,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -4235,7 +4235,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -4244,7 +4244,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -4257,7 +4257,7 @@ "snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", "dev": true, "requires": { "kind-of": "^3.2.0" @@ -4349,7 +4349,7 @@ "socket.io-parser": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.3.tgz", - "integrity": "sha512-g0a2HPqLguqAczs3dMECuA1RgoGFPyvDqcbaDEdCWY9g59kdUAz3YRmaJBNKXflrHNwB7Q12Gkf/0CZXfdHR7g==", + "integrity": "sha1-7S2l7nnxCVUDbj2kE7/X8eTYbI4=", "dev": true, "requires": { "component-emitter": "1.2.1", @@ -4379,7 +4379,7 @@ "socks-proxy-agent": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz", - "integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==", + "integrity": "sha1-Lq58+OKoLTRWV2FTmn+XGMVhdlk=", "dev": true, "requires": { "agent-base": "^4.1.0", @@ -4395,7 +4395,7 @@ "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", "dev": true, "requires": { "atob": "^2.1.1", @@ -4424,7 +4424,7 @@ "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", "dev": true, "requires": { "extend-shallow": "^3.0.0" @@ -4482,7 +4482,7 @@ "streamroller": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz", - "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==", + "integrity": "sha1-odG3z4PTmvsNYwSaWsv5NJO99ks=", "dev": true, "requires": { "date-format": "^1.2.0", @@ -4494,7 +4494,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "requires": { "safe-buffer": "~5.1.0" } @@ -4502,7 +4502,7 @@ "stringstream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "integrity": "sha1-eIAiWw1K0Q4wkn0Weh1vL9OzOnI=", "dev": true }, "strip-ansi": { @@ -4546,7 +4546,7 @@ "tar-stream": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "integrity": "sha1-+E7xaWJp1iI8pI9uHu7eP36B85U=", "dev": true, "requires": { "bl": "^1.0.0", @@ -4561,7 +4561,7 @@ "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", "dev": true, "requires": { "readable-stream": "^2.3.5", @@ -4587,7 +4587,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", "dev": true, "requires": { "os-tmpdir": "~1.0.2" @@ -4602,7 +4602,7 @@ "to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", "dev": true }, "to-object-path": { @@ -4628,7 +4628,7 @@ "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", "dev": true, "requires": { "define-property": "^2.0.2", @@ -4650,7 +4650,7 @@ "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "integrity": "sha1-7GDO44rGdQY//JelwYlwV47oNlU=", "dev": true, "requires": { "punycode": "^1.4.1" @@ -4721,7 +4721,7 @@ "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", "dev": true, "requires": { "media-typer": "0.3.0", @@ -4731,7 +4731,7 @@ "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", "dev": true }, "underscore": { @@ -4834,7 +4834,7 @@ "upath": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "integrity": "sha1-NSVll+RqWB20eT0M5H+prr/J+r0=", "dev": true }, "urix": { @@ -4846,7 +4846,7 @@ "use": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "integrity": "sha1-FHFr8D/f79AwQK71jYtLhfOnxUQ=", "dev": true, "requires": { "kind-of": "^6.0.2" @@ -4890,7 +4890,7 @@ "uws": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/uws/-/uws-9.14.0.tgz", - "integrity": "sha512-HNMztPP5A1sKuVFmdZ6BPVpBQd5bUjNC8EFMFiICK+oho/OQsAJy5hnIx4btMHiOk8j04f/DbIlqnEZ9d72dqg==", + "integrity": "sha1-+sg4a+/DOno3BcvVjcR7Qwyk3ZU=", "dev": true, "optional": true }, @@ -4972,7 +4972,7 @@ "boom": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", "dev": true, "requires": { "hoek": "4.x.x" @@ -4983,7 +4983,7 @@ "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", "dev": true, "requires": { "boom": "4.x.x", @@ -4995,7 +4995,7 @@ "hoek": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", + "integrity": "sha1-ljRQKqEsRF3Vp8VzS1cruHOKrLs=", "dev": true }, "q": { @@ -5007,7 +5007,7 @@ "request": { "version": "2.85.0", "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "integrity": "sha1-WgNhWkfGFCCz65m326IE+DYD4fo=", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -5037,7 +5037,7 @@ "sntp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", "dev": true, "requires": { "hoek": "4.x.x" @@ -5055,7 +5055,7 @@ "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", "dev": true, "requires": { "isexe": "^2.0.0" @@ -5084,7 +5084,7 @@ "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", "dev": true, "requires": { "async-limiter": "~1.0.0", diff --git a/samples/ClientSample/Program.cs b/samples/ClientSample/Program.cs index d049ce921a..0a581dbe6f 100644 --- a/samples/ClientSample/Program.cs +++ b/samples/ClientSample/Program.cs @@ -26,6 +26,8 @@ namespace ClientSample RawSample.Register(app); HubSample.Register(app); + StreamingSample.Register(app); + UploadSample.Register(app); app.Command("help", cmd => { diff --git a/samples/ClientSample/StreamingSample.cs b/samples/ClientSample/StreamingSample.cs new file mode 100644 index 0000000000..9ae84da9c2 --- /dev/null +++ b/samples/ClientSample/StreamingSample.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.CommandLineUtils; + +namespace ClientSample +{ + internal class StreamingSample + { + internal static void Register(CommandLineApplication app) + { + app.Command("streaming", cmd => + { + cmd.Description = "Tests a streaming connection to a hub"; + + var baseUrlArgument = cmd.Argument("", "The URL to the Chat Hub to test"); + + cmd.OnExecute(() => ExecuteAsync(baseUrlArgument.Value)); + }); + } + + public static async Task ExecuteAsync(string baseUrl) + { + var connection = new HubConnectionBuilder() + .WithUrl(baseUrl) + .Build(); + + await connection.StartAsync(); + + var reader = await connection.StreamAsChannelAsync("ChannelCounter", 10, 2000); + + while (await reader.WaitToReadAsync()) + { + while (reader.TryRead(out var item)) + { + Console.WriteLine($"received: {item}"); + } + } + + return 0; + } + } +} diff --git a/samples/ClientSample/UploadSample.cs b/samples/ClientSample/UploadSample.cs new file mode 100644 index 0000000000..b1b173b746 --- /dev/null +++ b/samples/ClientSample/UploadSample.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.CommandLineUtils; + +namespace ClientSample +{ + internal class UploadSample + { + internal static void Register(CommandLineApplication app) + { + app.Command("uploading", cmd => + { + cmd.Description = "Tests a streaming invocation from client to hub"; + + var baseUrlArgument = cmd.Argument("", "The URL to the Chat Hub to test"); + + cmd.OnExecute(() => ExecuteAsync(baseUrlArgument.Value)); + }); + } + + public static async Task ExecuteAsync(string baseUrl) + { + var connection = new HubConnectionBuilder() + .WithUrl(baseUrl) + .Build(); + await connection.StartAsync(); + + await BasicInvoke(connection); + //await MultiParamInvoke(connection); + //await AdditionalArgs(connection); + + return 0; + } + + public static async Task BasicInvoke(HubConnection connection) + { + var channel = Channel.CreateUnbounded(); + var invokeTask = connection.InvokeAsync("UploadWord", channel.Reader); + + foreach (var c in "hello") + { + await channel.Writer.WriteAsync(c.ToString()); + } + channel.Writer.TryComplete(); + + var result = await invokeTask; + Debug.WriteLine($"You message was: {result}"); + } + + private static async Task WriteStreamAsync(IEnumerable sequence, ChannelWriter writer) + { + foreach (T element in sequence) + { + await writer.WriteAsync(element); + await Task.Delay(100); + } + + writer.TryComplete(); + } + + public static async Task MultiParamInvoke(HubConnection connection) + { + var letters = Channel.CreateUnbounded(); + var numbers = Channel.CreateUnbounded(); + + _ = WriteStreamAsync(new[] { "h", "i", "!" }, letters.Writer); + _ = WriteStreamAsync(new[] { 1, 2, 3, 4, 5 }, numbers.Writer); + + var result = await connection.InvokeAsync("DoubleStreamUpload", letters.Reader, numbers.Reader); + + Debug.WriteLine(result); + } + + public static async Task AdditionalArgs(HubConnection connection) + { + var channel = Channel.CreateUnbounded(); + _ = WriteStreamAsync("main message".ToCharArray(), channel.Writer); + + var result = await connection.InvokeAsync("UploadWithSuffix", channel.Reader, " + wooh I'm a suffix"); + Debug.WriteLine($"Your message was: {result}"); + } + } +} + diff --git a/samples/SignalRSamples/Hubs/UploadHub.cs b/samples/SignalRSamples/Hubs/UploadHub.cs new file mode 100644 index 0000000000..bd821b5066 --- /dev/null +++ b/samples/SignalRSamples/Hubs/UploadHub.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; + +namespace SignalRSamples.Hubs +{ + public class UploadHub : Hub + { + public async Task DoubleStreamUpload(ChannelReader letters, ChannelReader numbers) + { + var total = await Sum(numbers); + var word = await UploadWord(letters); + + return string.Format("You sent over <{0}> <{1}s>", total, word); + } + + public async Task Sum(ChannelReader source) + { + var total = 0; + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + total += item; + } + } + return total; + } + + public async Task LocalSum(ChannelReader source) + { + var total = 0; + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + total += item; + } + } + Debug.WriteLine(String.Format("Complete, your total is <{0}>.", total)); + } + + public async Task UploadWord(ChannelReader source) + { + var sb = new StringBuilder(); + + // receiving a StreamCompleteMessage should cause this WaitToRead to return false + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + Debug.WriteLine($"received: {item}"); + Console.WriteLine($"received: {item}"); + sb.Append(item); + } + } + + // method returns, somewhere else returns a CompletionMessage with any errors + return sb.ToString(); + } + + public async Task UploadWithSuffix(ChannelReader source, string suffix) + { + var sb = new StringBuilder(); + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + await Task.Delay(50); + Debug.WriteLine($"received: {item}"); + sb.Append(item); + } + } + + sb.Append(suffix); + + return sb.ToString(); + } + + public async Task UploadFile(ChannelReader source, string filepath) + { + var result = Enumerable.Empty(); + int chunk = 1; + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + Debug.WriteLine($"received chunk #{chunk++}"); + result = result.Concat(item); // atrocious + await Task.Delay(50); + } + } + + File.WriteAllBytes(filepath, result.ToArray()); + + Debug.WriteLine("returning status code"); + return $"file written to '{filepath}'"; + } + } +} diff --git a/samples/SignalRSamples/Startup.cs b/samples/SignalRSamples/Startup.cs index e2cbd56b6a..89593f21cd 100644 --- a/samples/SignalRSamples/Startup.cs +++ b/samples/SignalRSamples/Startup.cs @@ -60,6 +60,7 @@ namespace SignalRSamples routes.MapHub("/dynamic"); routes.MapHub("/default"); routes.MapHub("/streaming"); + routes.MapHub("/uploading"); routes.MapHub("/hubT"); }); diff --git a/src/Common/ReflectionHelper.cs b/src/Common/ReflectionHelper.cs new file mode 100644 index 0000000000..eb11f635c1 --- /dev/null +++ b/src/Common/ReflectionHelper.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Channels; + +namespace Microsoft.AspNetCore.SignalR +{ + internal static class ReflectionHelper + { + public static bool IsStreamingType(Type type) + { + // IMPORTANT !! + // All valid types must be generic + // because HubConnectionContext gets the generic argument and uses it to determine the expected item type of the stream + // The long-term solution is making a (streaming type => expected item type) method. + + if (!type.IsGenericType) + { + return false; + } + + // walk up inheritance chain, until parent is either null or a ChannelReader + // TODO #2594 - add Streams here, to make sending files easy + while (type != null) + { + if (type.GetGenericTypeDefinition() == typeof(ChannelReader<>)) + { + return true; + } + + type = type.BaseType; + } + return false; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs index ed713cf001..8eafaedb20 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs @@ -186,6 +186,18 @@ namespace Microsoft.AspNetCore.SignalR.Client private static readonly Action _unableToAcquireConnectionLockForPing = LoggerMessage.Define(LogLevel.Trace, new EventId(62, "UnableToAcquireConnectionLockForPing"), "Skipping ping because a send is already in progress."); + private static readonly Action _startingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(63, "StartingStream"), "Initiating stream '{StreamId}'."); + + private static readonly Action _sendingStreamItem = + LoggerMessage.Define(LogLevel.Trace, new EventId(64, "StreamItemSent"), "Sending item for stream '{StreamId}'."); + + private static readonly Action _cancelingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(65, "CancelingStream"), "Stream '{StreamId}' has been canceled by client."); + + private static readonly Action _completingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(66, "CompletingStream"), "Sending completion message for stream '{StreamId}'."); + public static void PreparingNonBlockingInvocation(ILogger logger, string target, int count) { _preparingNonBlockingInvocation(logger, target, count, null); @@ -496,6 +508,26 @@ namespace Microsoft.AspNetCore.SignalR.Client { _unableToAcquireConnectionLockForPing(logger, null); } + + public static void StartingStream(ILogger logger, string streamId) + { + _startingStream(logger, streamId, null); + } + + public static void SendingStreamItem(ILogger logger, string streamId) + { + _sendingStreamItem(logger, streamId, null); + } + + public static void CancelingStream(ILogger logger, string streamId) + { + _cancelingStream(logger, streamId, null); + } + + public static void CompletingStream(ILogger logger, string streamId) + { + _completingStream(logger, streamId, null); + } } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index 47f4adf4c6..634c5b8849 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; @@ -37,6 +39,8 @@ namespace Microsoft.AspNetCore.SignalR.Client // This lock protects the connection state. private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1); + private static readonly MethodInfo _sendStreamItemsMethod = typeof(HubConnection).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Single(m => m.Name.Equals("SendStreamItems")); + // Persistent across all connections private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; @@ -44,10 +48,13 @@ namespace Microsoft.AspNetCore.SignalR.Client private readonly IServiceProvider _serviceProvider; private readonly IConnectionFactory _connectionFactory; private readonly ConcurrentDictionary _handlers = new ConcurrentDictionary(StringComparer.Ordinal); + private long _nextActivationServerTimeout; private long _nextActivationSendPing; private bool _disposed; + private CancellationToken _uploadStreamToken; + private readonly ConnectionLogScope _logScope; // Transient state to a connection @@ -419,6 +426,7 @@ namespace Microsoft.AspNetCore.SignalR.Client CheckDisposed(); CheckConnectionActive(nameof(StreamAsChannelCoreAsync)); + // I just want an excuse to use 'irq' as a variable name... var irq = InvocationRequest.Stream(cancellationToken, returnType, _connectionState.GetNextId(), _loggerFactory, this, out channel); await InvokeStreamCore(methodName, irq, args, cancellationToken); @@ -435,9 +443,84 @@ namespace Microsoft.AspNetCore.SignalR.Client return channel; } + private Dictionary PackageStreamingParams(object[] args) + { + // lazy initialized, to avoid allocation unecessary dictionaries + Dictionary readers = null; + + for (var i = 0; i < args.Length; i++) + { + if (ReflectionHelper.IsStreamingType(args[i].GetType())) + { + if (readers == null) + { + readers = new Dictionary(); + } + + var id = _connectionState.GetNextStreamId(); + readers[id] = args[i]; + args[i] = new StreamPlaceholder(id); + + Log.StartingStream(_logger, id); + } + } + + return readers; + } + + private void LaunchStreams(Dictionary readers, CancellationToken cancellationToken) + { + if (readers == null) + { + // if there were no streaming parameters then readers is never initialized + return; + } + foreach (var kvp in readers) + { + var reader = kvp.Value; + + // For each stream that needs to be sent, run a "send items" task in the background. + // This reads from the channel, attaches streamId, and sends to server. + // A single background thread here quickly gets messy. + _ = _sendStreamItemsMethod + .MakeGenericMethod(reader.GetType().GetGenericArguments()) + .Invoke(this, new object[] { kvp.Key.ToString(), reader, cancellationToken }); + } + } + + // this is called via reflection using the `_sendStreamItems` field + private async Task SendStreamItems(string streamId, ChannelReader reader, CancellationToken token) + { + Log.StartingStream(_logger, streamId); + + var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_uploadStreamToken, token).Token; + + string responseError = null; + try + { + while (await reader.WaitToReadAsync(combinedToken)) + { + while (!combinedToken.IsCancellationRequested && reader.TryRead(out var item)) + { + await SendWithLock(new StreamDataMessage(streamId, item)); + Log.SendingStreamItem(_logger, streamId); + } + } + } + catch (OperationCanceledException) + { + Log.CancelingStream(_logger, streamId); + responseError = $"Stream canceled by client."; + } + + Log.CompletingStream(_logger, streamId); + await SendWithLock(new StreamCompleteMessage(streamId, responseError)); + } private async Task InvokeCoreAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken) { + var readers = PackageStreamingParams(args); + CheckDisposed(); await WaitConnectionLockAsync(); @@ -455,21 +538,20 @@ namespace Microsoft.AspNetCore.SignalR.Client ReleaseConnectionLock(); } - // Wait for this outside the lock, because it won't complete until the server responds. + LaunchStreams(readers, cancellationToken); + + // Wait for this outside the lock, because it won't complete until the server responds return await invocationTask; } private async Task InvokeCore(string methodName, InvocationRequest irq, object[] args, CancellationToken cancellationToken) { - AssertConnectionValid(); - Log.PreparingBlockingInvocation(_logger, irq.InvocationId, methodName, irq.ResultType.FullName, args.Length); // Client invocations are always blocking var invocationMessage = new InvocationMessage(irq.InvocationId, methodName, args); Log.RegisteringInvocation(_logger, invocationMessage.InvocationId); - _connectionState.AddInvocation(irq); // Trace the full invocation @@ -495,7 +577,6 @@ namespace Microsoft.AspNetCore.SignalR.Client var invocationMessage = new StreamInvocationMessage(irq.InvocationId, methodName, args); - // I just want an excuse to use 'irq' as a variable name... Log.RegisteringInvocation(_logger, invocationMessage.InvocationId); _connectionState.AddInvocation(irq); @@ -525,28 +606,33 @@ namespace Microsoft.AspNetCore.SignalR.Client // REVIEW: If a token is passed in and is canceled during FlushAsync it seems to break .Complete()... await _connectionState.Connection.Transport.Output.FlushAsync(); + Log.MessageSent(_logger, hubMessage); // We've sent a message, so don't ping for a while ResetSendPing(); - - Log.MessageSent(_logger, hubMessage); } private async Task SendCoreAsyncCore(string methodName, object[] args, CancellationToken cancellationToken) { - CheckDisposed(); + var readers = PackageStreamingParams(args); + Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length); + + var invocationMessage = new InvocationMessage(null, methodName, args); + await SendWithLock(invocationMessage, callerName: nameof(SendCoreAsync)); + + LaunchStreams(readers, cancellationToken); + } + + private async Task SendWithLock(HubMessage message, CancellationToken cancellationToken = default, [CallerMemberName] string callerName = "") + { + CheckDisposed(); await WaitConnectionLockAsync(); try { + CheckConnectionActive(callerName); CheckDisposed(); - CheckConnectionActive(nameof(SendCoreAsync)); - - Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length); - - var invocationMessage = new InvocationMessage(null, methodName, args); - - await SendHubMessage(invocationMessage, cancellationToken); + await SendHubMessage(message, cancellationToken); } finally { @@ -575,15 +661,15 @@ namespace Microsoft.AspNetCore.SignalR.Client if (!connectionState.TryRemoveInvocation(completion.InvocationId, out irq)) { Log.DroppedCompletionMessage(_logger, completion.InvocationId); + break; } - else - { - DispatchInvocationCompletion(completion, irq); - irq.Dispose(); - } + + DispatchInvocationCompletion(completion, irq); + irq.Dispose(); + break; case StreamItemMessage streamItem: - // Complete the invocation with an error, we don't support streaming (yet) + // if there's no open StreamInvocation with the given id, then complete with an error if (!connectionState.TryGetInvocation(streamItem.InvocationId, out irq)) { Log.DroppedStreamMessage(_logger, streamItem.InvocationId); @@ -767,6 +853,9 @@ namespace Microsoft.AspNetCore.SignalR.Client var timer = new TimerAwaitable(TickRate, TickRate); _ = TimerLoop(timer); + var uploadStreamSource = new CancellationTokenSource(); + _uploadStreamToken = uploadStreamSource.Token; + try { while (true) @@ -834,6 +923,7 @@ namespace Microsoft.AspNetCore.SignalR.Client finally { timer.Stop(); + uploadStreamSource.Cancel(); } // Clear the connectionState field @@ -916,11 +1006,6 @@ namespace Microsoft.AspNetCore.SignalR.Client private void OnServerTimeout() { - if (Debugger.IsAttached) - { - return; - } - _connectionState.CloseException = new TimeoutException( $"Server timeout ({ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server."); _connectionState.Connection.Transport.Input.CancelPendingRead(); @@ -1104,7 +1189,8 @@ namespace Microsoft.AspNetCore.SignalR.Client private TaskCompletionSource _stopTcs; private readonly object _lock = new object(); private readonly Dictionary _pendingCalls = new Dictionary(StringComparer.Ordinal); - private int _nextId; + private int _nextInvocationId; + private int _nextStreamId; public ConnectionContext Connection { get; } public Task ReceiveTask { get; set; } @@ -1125,7 +1211,8 @@ namespace Microsoft.AspNetCore.SignalR.Client Connection = connection; } - public string GetNextId() => Interlocked.Increment(ref _nextId).ToString(CultureInfo.InvariantCulture); + public string GetNextId() => Interlocked.Increment(ref _nextInvocationId).ToString(CultureInfo.InvariantCulture); + public string GetNextStreamId() => Interlocked.Increment(ref _nextStreamId).ToString(CultureInfo.InvariantCulture); public void AddInvocation(InvocationRequest irq) { @@ -1232,6 +1319,18 @@ namespace Microsoft.AspNetCore.SignalR.Client return irq.ResultType; } + Type IInvocationBinder.GetStreamItemType(string invocationId) + { + // previously, streaming was only server->client, and used GetReturnType for StreamItems + // literally the same code as the above method + if (!TryGetInvocation(invocationId, out var irq)) + { + Log.ReceivedUnexpectedResponse(_hubConnection._logger, invocationId); + return null; + } + return irq.ResultType; + } + IReadOnlyList IInvocationBinder.GetParameterTypes(string methodName) { if (!_hubConnection._handlers.TryGetValue(methodName, out var invocationHandlerList)) diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj b/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj index 8cbe6845a4..bc9ad15161 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs b/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs index 4e8fd1bcb5..2f1ba139c3 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs @@ -10,5 +10,6 @@ namespace Microsoft.AspNetCore.SignalR { Type GetReturnType(string invocationId); IReadOnlyList GetParameterTypes(string methodName); + Type GetStreamItemType(string streamId); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs index ce1e3cbfd5..25fbf6dbbc 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs @@ -42,5 +42,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol /// Represents the close message type. /// public const int CloseMessageType = 7; + + /// + /// Represents the stream complete message type. + /// + public const int StreamCompleteMessageType = 8; + + /// + /// Same as StreamItemMessage, except + /// + public const int StreamDataMessageType = 9; } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs new file mode 100644 index 0000000000..571e1fdc39 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Text; + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// Represents a failure to bind arguments for a StreamDataMessage. This does not represent an actual + /// message that is sent on the wire, it is returned by + /// to indicate that a binding failure occurred when parsing a StreamDataMessage. The stream ID is associated + /// so that the error can be sent to the relevant hub method. + /// + public class StreamBindingFailureMessage : HubMessage + { + /// + /// Gets the id of the relevant stream + /// + public string Id { get; } + + /// + /// Gets the exception thrown during binding. + /// + public ExceptionDispatchInfo BindingFailure { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The stream ID. + /// The exception thrown during binding. + public StreamBindingFailureMessage(string id, ExceptionDispatchInfo bindingFailure) + { + Id = id; + BindingFailure = bindingFailure; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs new file mode 100644 index 0000000000..587764aa72 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// A message for indicating that a particular stream has ended. + /// + public class StreamCompleteMessage : HubMessage + { + /// + /// Gets the stream id. + /// + public string StreamId { get; } + + /// + /// Gets the error. Will be null if there is no error. + /// + public string Error { get; } + + /// + /// Whether the message has an error. + /// + public bool HasError { get => Error != null; } + + /// + /// Initializes a new instance of + /// + /// The streamId of the stream to complete. + /// An optional error field. + public StreamCompleteMessage(string streamId, string error = null) + { + StreamId = streamId; + Error = error; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs new file mode 100644 index 0000000000..6862ed96a2 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// Sent to parameter streams. + /// Similar to , except the data is sent to a parameter stream, rather than in response to an invocation. + /// + public class StreamDataMessage : HubMessage + { + /// + /// The piece of data this message carries. + /// + public object Item { get; } + + /// + /// The stream to which to deliver data. + /// + public string StreamId { get; } + + public StreamDataMessage(string streamId, object item) + { + StreamId = streamId; + Item = item; + } + + public override string ToString() + { + return $"StreamDataMessage {{ {nameof(StreamId)}: \"{StreamId}\", {nameof(Item)}: {Item ?? "<>"} }}"; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs new file mode 100644 index 0000000000..f111e90cba --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// Used by protocol serializers/deserializers to transfer information about streaming parameters. + /// Is packed as an argument in the form `{"streamId": "42"}`, and sent over wire. + /// Is then unpacked on the other side, and a new channel is created and saved under the streamId. + /// Then, each is routed to the appropiate channel based on streamId. + /// + public class StreamPlaceholder + { + public string StreamId { get; private set; } + + public StreamPlaceholder(string streamId) + { + StreamId = streamId; + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json index 3cd71f82e7..ac9bc43e9f 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json +++ b/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json @@ -8,5 +8,10 @@ "TypeId": "public interface Microsoft.AspNetCore.SignalR.Protocol.IHubProtocol", "MemberId": "System.Int32 get_MinorVersion()", "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.AspNetCore.SignalR.IInvocationBinder", + "MemberId": "System.Type GetStreamItemType(System.String streamId)", + "Kind": "Addition" } ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs index 5aaa164cea..14a2fa6ac4 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs @@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.SignalR { public class HubConnectionContext { + private StreamTracker _streamTracker; private static readonly WaitCallback _abortedCallback = AbortConnection; private readonly ConnectionContext _connectionContext; @@ -54,6 +55,18 @@ namespace Microsoft.AspNetCore.SignalR _clientTimeoutInterval = clientTimeoutInterval.Ticks; } + internal StreamTracker StreamTracker + { + get + { + // lazy for performance reasons + if (_streamTracker == null) + { + _streamTracker = new StreamTracker(); + } + return _streamTracker; + } + } /// /// Initializes a new instance of the class. /// diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs index 9211fda589..85e765571c 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs @@ -186,6 +186,9 @@ namespace Microsoft.AspNetCore.SignalR { var input = connection.Input; var protocol = connection.Protocol; + + var binder = new HubConnectionBinder(_dispatcher, connection); + while (true) { var result = await input.ReadAsync(); @@ -202,7 +205,7 @@ namespace Microsoft.AspNetCore.SignalR { connection.ResetClientTimeout(); - while (protocol.TryParseMessage(ref buffer, _dispatcher, out var message)) + while (protocol.TryParseMessage(ref buffer, binder, out var message)) { await _dispatcher.DispatchMessageAsync(connection, message); } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs index 9e05de55d2..096b4ec229 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs @@ -57,6 +57,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal private static readonly Action _invalidReturnValueFromStreamingMethod = LoggerMessage.Define(LogLevel.Error, new EventId(15, "InvalidReturnValueFromStreamingMethod"), "A streaming method returned a value that cannot be used to build enumerator {HubMethod}."); + private static readonly Action _receivedStreamItem = + LoggerMessage.Define(LogLevel.Trace, new EventId(16, "ReceivedStreamItem"), "Received item for stream '{StreamId}'."); + + private static readonly Action _startingParameterStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(17, "StartingParameterStream"), "Creating streaming parameter channel '{StreamId}'."); + + private static readonly Action _completingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(18, "CompletingStream"), "Stream '{StreamId}' has been completed by client."); + + private static readonly Action _closingStreamWithBindingError = + LoggerMessage.Define(LogLevel.Warning, new EventId(19, "ClosingStreamWithBindingError"), "Stream '{StreamId}' closed with error '{Error}'."); + public static void ReceivedHubInvocation(ILogger logger, InvocationMessage invocationMessage) { _receivedHubInvocation(logger, invocationMessage, null); @@ -133,6 +145,26 @@ namespace Microsoft.AspNetCore.SignalR.Internal { _invalidReturnValueFromStreamingMethod(logger, hubMethod, null); } + + public static void ReceivedStreamItem(ILogger logger, StreamDataMessage message) + { + _receivedStreamItem(logger, message.StreamId, null); + } + + public static void StartingParameterStream(ILogger logger, string streamId) + { + _startingParameterStream(logger, streamId, null); + } + + public static void CompletingStream(ILogger logger, StreamCompleteMessage message) + { + _completingStream(logger, message.StreamId, null); + } + + public static void ClosingStreamWithBindingError(ILogger logger, StreamCompleteMessage message) + { + _closingStreamWithBindingError(logger, message.StreamId, message.Error, null); + } } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs index 4f1fc15668..5a927ffc71 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs @@ -81,15 +81,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal switch (hubMessage) { case InvocationBindingFailureMessage bindingFailureMessage: - return ProcessBindingFailure(connection, bindingFailureMessage); + return ProcessInvocationBindingFailure(connection, bindingFailureMessage); + + case StreamBindingFailureMessage bindingFailureMessage: + return ProcessStreamBindingFailure(connection, bindingFailureMessage); case InvocationMessage invocationMessage: Log.ReceivedHubInvocation(_logger, invocationMessage); - return ProcessInvocation(connection, invocationMessage, isStreamedInvocation: false); + return ProcessInvocation(connection, invocationMessage, isStreamResponse: false); case StreamInvocationMessage streamInvocationMessage: Log.ReceivedStreamHubInvocation(_logger, streamInvocationMessage); - return ProcessInvocation(connection, streamInvocationMessage, isStreamedInvocation: true); + return ProcessInvocation(connection, streamInvocationMessage, isStreamResponse: true); case CancelInvocationMessage cancelInvocationMessage: // Check if there is an associated active stream and cancel it if it exists. @@ -110,6 +113,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal connection.StartClientTimeout(); break; + case StreamDataMessage streamItem: + Log.ReceivedStreamItem(_logger, streamItem); + return ProcessStreamItem(connection, streamItem); + + case StreamCompleteMessage streamCompleteMessage: + // closes channels, removes from Lookup dict + // user's method can see the channel is complete and begin wrapping up + Log.CompletingStream(_logger, streamCompleteMessage); + connection.StreamTracker.Complete(streamCompleteMessage); + break; + // Other kind of message we weren't expecting default: Log.UnsupportedMessageReceived(_logger, hubMessage.GetType().FullName); @@ -119,30 +133,37 @@ namespace Microsoft.AspNetCore.SignalR.Internal return Task.CompletedTask; } - private Task ProcessBindingFailure(HubConnectionContext connection, InvocationBindingFailureMessage bindingFailureMessage) + private Task ProcessInvocationBindingFailure(HubConnectionContext connection, InvocationBindingFailureMessage bindingFailureMessage) { Log.FailedInvokingHubMethod(_logger, bindingFailureMessage.Target, bindingFailureMessage.BindingFailure.SourceException); + + var errorMessage = ErrorMessageHelper.BuildErrorMessage($"Failed to invoke '{bindingFailureMessage.Target}' due to an error on the server.", bindingFailureMessage.BindingFailure.SourceException, _enableDetailedErrors); return SendInvocationError(bindingFailureMessage.InvocationId, connection, errorMessage); } - public override Type GetReturnType(string invocationId) + private Task ProcessStreamBindingFailure(HubConnectionContext connection, StreamBindingFailureMessage bindingFailureMessage) { - return typeof(object); + var errorString = ErrorMessageHelper.BuildErrorMessage( + $"Failed to bind Stream Item arguments to proper type.", + bindingFailureMessage.BindingFailure.SourceException, _enableDetailedErrors); + + var message = new StreamCompleteMessage(bindingFailureMessage.Id, errorString); + Log.ClosingStreamWithBindingError(_logger, message); + connection.StreamTracker.Complete(message); + + return Task.CompletedTask; } - public override IReadOnlyList GetParameterTypes(string methodName) + private Task ProcessStreamItem(HubConnectionContext connection, StreamDataMessage message) { - if (!_methods.TryGetValue(methodName, out var descriptor)) - { - return Type.EmptyTypes; - } - return descriptor.ParameterTypes; + Log.ReceivedStreamItem(_logger, message); + return connection.StreamTracker.ProcessItem(message); } private Task ProcessInvocation(HubConnectionContext connection, - HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamedInvocation) + HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamResponse) { if (!_methods.TryGetValue(hubMethodInvocationMessage.Target, out var descriptor)) { @@ -153,12 +174,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal } else { - return Invoke(descriptor, connection, hubMethodInvocationMessage, isStreamedInvocation); + bool isStreamCall = descriptor.HasStreamingParameters; + if (isStreamResponse && isStreamCall) + { + throw new NotSupportedException("Streaming responses for streaming uploads are not supported."); + } + return Invoke(descriptor, connection, hubMethodInvocationMessage, isStreamResponse, isStreamCall); } } private async Task Invoke(HubMethodDescriptor descriptor, HubConnectionContext connection, - HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamedInvocation) + HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamResponse, bool isStreamCall) { var methodExecutor = descriptor.MethodExecutor; @@ -176,7 +202,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal return; } - if (!await ValidateInvocationMode(descriptor, isStreamedInvocation, hubMethodInvocationMessage, connection)) + if (!await ValidateInvocationMode(descriptor, isStreamResponse, hubMethodInvocationMessage, connection)) { return; } @@ -184,33 +210,73 @@ namespace Microsoft.AspNetCore.SignalR.Internal hubActivator = scope.ServiceProvider.GetRequiredService>(); hub = hubActivator.Create(); + if (isStreamCall) + { + // swap out placeholders for channels + var args = hubMethodInvocationMessage.Arguments; + for (int i = 0; i < args.Length; i++) + { + var placeholder = args[i] as StreamPlaceholder; + if (placeholder == null) + { + continue; + } + + Log.StartingParameterStream(_logger, placeholder.StreamId); + var itemType = methodExecutor.MethodParameters[i].ParameterType.GetGenericArguments()[0]; + args[i] = connection.StreamTracker.AddStream(placeholder.StreamId, itemType); + } + } + try { InitializeHub(hub, connection); + Task invocation = null; - var result = await ExecuteHubMethod(methodExecutor, hub, hubMethodInvocationMessage.Arguments); - - if (isStreamedInvocation) + if (isStreamResponse) { + var result = await ExecuteHubMethod(methodExecutor, hub, hubMethodInvocationMessage.Arguments); + if (!TryGetStreamingEnumerator(connection, hubMethodInvocationMessage.InvocationId, descriptor, result, out var enumerator, out var streamCts)) { Log.InvalidReturnValueFromStreamingMethod(_logger, methodExecutor.MethodInfo.Name); - await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection, $"The value returned by the streaming method '{methodExecutor.MethodInfo.Name}' is not a ChannelReader<>."); return; } - disposeScope = false; Log.StreamingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor); - // Fire-and-forget stream invocations, otherwise they would block other hub invocations from being able to run _ = StreamResultsAsync(hubMethodInvocationMessage.InvocationId, connection, enumerator, scope, hubActivator, hub, streamCts); } - // Non-empty/null InvocationId ==> Blocking invocation that needs a response - else if (!string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId)) + + else if (string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId)) { - Log.SendingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor); - await connection.WriteAsync(CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result)); + // Send Async, no response expected + invocation = ExecuteHubMethod(methodExecutor, hub, hubMethodInvocationMessage.Arguments); + } + + else + { + // Invoke Async, one reponse expected + async Task ExecuteInvocation() + { + var result = await ExecuteHubMethod(methodExecutor, hub, hubMethodInvocationMessage.Arguments); + Log.SendingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor); + await connection.WriteAsync(CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result)); + } + invocation = ExecuteInvocation(); + } + + if (isStreamCall || isStreamResponse) + { + // don't await streaming invocations + // leave them running in the background, allowing dispatcher to process other messages between streaming items + disposeScope = false; + } + else + { + // complete the non-streaming calls now + await invocation; } } catch (TargetInvocationException ex) @@ -236,7 +302,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal } } - private async Task StreamResultsAsync(string invocationId, HubConnectionContext connection, IAsyncEnumerator enumerator, IServiceScope scope, IHubActivator hubActivator, THub hub, CancellationTokenSource streamCts) + private async Task StreamResultsAsync(string invocationId, HubConnectionContext connection, IAsyncEnumerator enumerator, IServiceScope scope, + IHubActivator hubActivator, THub hub, CancellationTokenSource streamCts) { string error = null; @@ -424,5 +491,15 @@ namespace Microsoft.AspNetCore.SignalR.Internal Log.HubMethodBound(_logger, hubName, methodName); } } + + public override IReadOnlyList GetParameterTypes(string methodName) + { + if (!_methods.TryGetValue(methodName, out var descriptor)) + { + return Type.EmptyTypes; + } + + return descriptor.ParameterTypes; + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs new file mode 100644 index 0000000000..dcd4bd5c7d --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.SignalR.Internal; + +namespace Microsoft.AspNetCore.SignalR.Internal +{ + internal class HubConnectionBinder : IInvocationBinder where THub : Hub + { + private HubDispatcher _dispatcher; + private HubConnectionContext _connection; + + public HubConnectionBinder(HubDispatcher dispatcher, HubConnectionContext connection) + { + _dispatcher = dispatcher; + _connection = connection; + } + + public IReadOnlyList GetParameterTypes(string methodName) + { + return _dispatcher.GetParameterTypes(methodName); + } + + public Type GetReturnType(string invocationId) + { + return typeof(object); + } + + public Type GetStreamItemType(string streamId) + { + return _connection.StreamTracker.GetStreamItemType(streamId); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs index 5d787fb476..9bd545da50 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs @@ -8,12 +8,11 @@ using Microsoft.AspNetCore.SignalR.Protocol; namespace Microsoft.AspNetCore.SignalR.Internal { - public abstract class HubDispatcher : IInvocationBinder where THub : Hub + public abstract class HubDispatcher where THub : Hub { public abstract Task OnConnectedAsync(HubConnectionContext connection); public abstract Task OnDisconnectedAsync(HubConnectionContext connection, Exception exception); public abstract Task DispatchMessageAsync(HubConnectionContext connection, HubMessage hubMessage); - public abstract IReadOnlyList GetParameterTypes(string methodName); - public abstract Type GetReturnType(string invocationId); + public abstract IReadOnlyList GetParameterTypes(string name); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs index a15dce772e..b942279e46 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading; using System.Threading.Channels; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.SignalR.Internal @@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IEnumerable policies) { MethodExecutor = methodExecutor; - ParameterTypes = methodExecutor.MethodParameters.Select(p => p.ParameterType).ToArray(); + ParameterTypes = methodExecutor.MethodParameters.Select(GetParameterType).ToArray(); Policies = policies.ToArray(); NonAsyncReturnType = (MethodExecutor.IsMethodAsync) @@ -36,6 +37,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal } } + public bool HasStreamingParameters { get; private set; } + private Func> _convertToEnumerator; public ObjectMethodExecutor MethodExecutor { get; } @@ -52,6 +55,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal public IList Policies { get; } + private Type GetParameterType(ParameterInfo p) + { + var type = p.ParameterType; + if (ReflectionHelper.IsStreamingType(type)) + { + HasStreamingParameters = true; + return typeof(StreamPlaceholder); + } + return type; + } + private static bool IsChannelType(Type type, out Type payloadType) { var channelType = type.AllBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ChannelReader<>)); diff --git a/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj b/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj index d63b5aed42..012aca906c 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj @@ -6,6 +6,10 @@ Microsoft.AspNetCore.SignalR + + + + diff --git a/src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs b/src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs new file mode 100644 index 0000000000..3d36e38c0b --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.SignalR +{ + internal class StreamTracker + { + private static readonly MethodInfo _buildConverterMethod = typeof(StreamTracker).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Single(m => m.Name.Equals("BuildStream")); + private ConcurrentDictionary _lookup = new ConcurrentDictionary(); + + /// + /// Creates a new stream and returns the ChannelReader for it as an object. + /// + public object AddStream(string streamId, Type itemType) + { + var newConverter = (IStreamConverter)_buildConverterMethod.MakeGenericMethod(itemType).Invoke(null, Array.Empty()); + _lookup[streamId] = newConverter; + return newConverter.GetReaderAsObject(); + } + + private IStreamConverter TryGetConverter(string streamId) + { + if (_lookup.TryGetValue(streamId, out var converter)) + { + return converter; + } + else + { + throw new KeyNotFoundException($"No stream with id '{streamId}' could be found."); + } + } + + public Task ProcessItem(StreamDataMessage message) + { + return TryGetConverter(message.StreamId).WriteToStream(message.Item); + } + + public Type GetStreamItemType(string streamId) + { + return TryGetConverter(streamId).GetItemType(); + } + + public void Complete(StreamCompleteMessage message) + { + _lookup.TryRemove(message.StreamId, out var converter); + if (converter == null) + { + throw new KeyNotFoundException($"No stream with id '{message.StreamId}' could be found."); + } + converter.TryComplete(message.HasError ? new Exception(message.Error) : null); + } + + private static IStreamConverter BuildStream() + { + return new ChannelConverter(); + } + + private interface IStreamConverter + { + Type GetItemType(); + object GetReaderAsObject(); + Task WriteToStream(object item); + void TryComplete(Exception ex); + } + + private class ChannelConverter : IStreamConverter + { + private Channel _channel; + + public ChannelConverter() + { + _channel = Channel.CreateUnbounded(); + } + + public Type GetItemType() + { + return typeof(T); + } + + public object GetReaderAsObject() + { + return _channel.Reader; + } + + public Task WriteToStream(object o) + { + return _channel.Writer.WriteAsync((T)o).AsTask(); + } + + public void TryComplete(Exception ex) + { + _channel.Writer.TryComplete(ex); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj b/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj index 47f37af697..7daf883097 100644 --- a/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj +++ b/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj @@ -1,4 +1,4 @@ - + Implements the SignalR Hub Protocol over JSON. diff --git a/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs index 54400a2199..8436d0aa68 100644 --- a/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private const string ResultPropertyName = "result"; private const string ItemPropertyName = "item"; private const string InvocationIdPropertyName = "invocationId"; + private const string StreamIdPropertyName = "streamId"; private const string TypePropertyName = "type"; private const string ErrorPropertyName = "error"; private const string TargetPropertyName = "target"; @@ -119,6 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol int? type = null; string invocationId = null; + string streamId = null; string target = null; string error = null; var hasItem = false; @@ -165,6 +167,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol case InvocationIdPropertyName: invocationId = JsonUtils.ReadAsString(reader, InvocationIdPropertyName); break; + case StreamIdPropertyName: + streamId = JsonUtils.ReadAsString(reader, StreamIdPropertyName); + break; case TargetPropertyName: target = JsonUtils.ReadAsString(reader, TargetPropertyName); break; @@ -199,15 +204,32 @@ namespace Microsoft.AspNetCore.SignalR.Protocol hasItem = true; - if (string.IsNullOrEmpty(invocationId)) + + string id = null; + if (!string.IsNullOrEmpty(invocationId)) { - // If we don't have an invocation id then we need to store it as a JToken so we can parse it later - itemToken = JToken.Load(reader); + id = invocationId; + } + else if (!string.IsNullOrEmpty(streamId)) + { + id = streamId; } else { - var returnType = binder.GetReturnType(invocationId); - item = PayloadSerializer.Deserialize(reader, returnType); + // If we don't have an id yetmthen we need to store it as a JToken to parse later + itemToken = JToken.Load(reader); + break; + } + + Type itemType = binder.GetStreamItemType(id); + + try + { + item = PayloadSerializer.Deserialize(reader, itemType); + } + catch (JsonSerializationException ex) + { + return new StreamBindingFailureMessage(id, ExceptionDispatchInfo.Capture(ex)); } break; case ArgumentsPropertyName: @@ -313,11 +335,33 @@ namespace Microsoft.AspNetCore.SignalR.Protocol : BindStreamInvocationMessage(invocationId, target, arguments, hasArguments, binder); } break; + case HubProtocolConstants.StreamDataMessageType: + if (itemToken != null) + { + var itemType = binder.GetStreamItemType(streamId); + try + { + item = itemToken.ToObject(itemType, PayloadSerializer); + } + catch (JsonSerializationException ex) + { + return new StreamBindingFailureMessage(streamId, ExceptionDispatchInfo.Capture(ex)); + } + } + message = BindParamStreamMessage(streamId, item, hasItem, binder); + break; case HubProtocolConstants.StreamItemMessageType: if (itemToken != null) { - var returnType = binder.GetReturnType(invocationId); - item = itemToken.ToObject(returnType, PayloadSerializer); + var returnType = binder.GetStreamItemType(invocationId); + try + { + item = itemToken.ToObject(returnType, PayloadSerializer); + } + catch (JsonSerializationException ex) + { + return new StreamBindingFailureMessage(invocationId, ExceptionDispatchInfo.Capture(ex)); + }; } message = BindStreamItemMessage(invocationId, item, hasItem, binder); @@ -338,6 +382,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return PingMessage.Instance; case HubProtocolConstants.CloseMessageType: return BindCloseMessage(error); + case HubProtocolConstants.StreamCompleteMessageType: + message = BindStreamCompleteMessage(streamId, error); + break; case null: throw new InvalidDataException($"Missing required property '{TypePropertyName}'."); default: @@ -408,6 +455,10 @@ namespace Microsoft.AspNetCore.SignalR.Protocol WriteHeaders(writer, m); WriteStreamInvocationMessage(m, writer); break; + case StreamDataMessage m: + WriteMessageType(writer, HubProtocolConstants.StreamDataMessageType); + WriteStreamDataMessage(m, writer); + break; case StreamItemMessage m: WriteMessageType(writer, HubProtocolConstants.StreamItemMessageType); WriteHeaders(writer, m); @@ -430,6 +481,10 @@ namespace Microsoft.AspNetCore.SignalR.Protocol WriteMessageType(writer, HubProtocolConstants.CloseMessageType); WriteCloseMessage(m, writer); break; + case StreamCompleteMessage m: + WriteMessageType(writer, HubProtocolConstants.StreamCompleteMessageType); + WriteStreamCompleteMessage(m, writer); + break; default: throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}"); } @@ -478,6 +533,18 @@ namespace Microsoft.AspNetCore.SignalR.Protocol WriteInvocationId(message, writer); } + private void WriteStreamCompleteMessage(StreamCompleteMessage message, JsonTextWriter writer) + { + writer.WritePropertyName(StreamIdPropertyName); + writer.WriteValue(message.StreamId); + + if (message.Error != null) + { + writer.WritePropertyName(ErrorPropertyName); + writer.WriteValue(message.Error); + } + } + private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer) { WriteInvocationId(message, writer); @@ -485,6 +552,14 @@ namespace Microsoft.AspNetCore.SignalR.Protocol PayloadSerializer.Serialize(writer, message.Item); } + private void WriteStreamDataMessage(StreamDataMessage message, JsonTextWriter writer) + { + writer.WritePropertyName(StreamIdPropertyName); + writer.WriteValue(message.StreamId); + writer.WritePropertyName(ItemPropertyName); + PayloadSerializer.Serialize(writer, message.Item); + } + private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer) { WriteInvocationId(message, writer); @@ -548,6 +623,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new CancelInvocationMessage(invocationId); } + private HubMessage BindStreamCompleteMessage(string streamId, string error) + { + if (string.IsNullOrEmpty(streamId)) + { + throw new InvalidDataException($"Missing required property '{StreamIdPropertyName}'."); + } + + // note : if the stream completes normally, the error should be `null` + return new StreamCompleteMessage(streamId, error); + } + private HubMessage BindCompletionMessage(string invocationId, string error, object result, bool hasResult, IInvocationBinder binder) { if (string.IsNullOrEmpty(invocationId)) @@ -568,6 +654,20 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new CompletionMessage(invocationId, error, result: null, hasResult: false); } + private HubMessage BindParamStreamMessage(string streamId, object item, bool hasItem, IInvocationBinder binder) + { + if (string.IsNullOrEmpty(streamId)) + { + throw new InvalidDataException($"Missing required property '{StreamIdPropertyName}"); + } + if (!hasItem) + { + throw new InvalidDataException($"Missing required property '{ItemPropertyName}"); + } + + return new StreamDataMessage(streamId, item); + } + private HubMessage BindStreamItemMessage(string invocationId, object item, bool hasItem, IInvocationBinder binder) { if (string.IsNullOrEmpty(invocationId)) @@ -658,7 +758,6 @@ namespace Microsoft.AspNetCore.SignalR.Protocol { if (paramIndex < paramCount) { - // Set all known arguments arguments[paramIndex] = PayloadSerializer.Deserialize(reader, paramTypes[paramIndex]); } else diff --git a/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs index 6f6a76252e..ffeb7bfe75 100644 --- a/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private static HubMessage ParseMessage(byte[] input, int startOffset, IInvocationBinder binder, IFormatterResolver resolver) { - _ = MessagePackBinary.ReadArrayHeader(input, startOffset, out var readSize); + MessagePackBinary.ReadArrayHeader(input, startOffset, out var readSize); startOffset += readSize; var messageType = ReadInt32(input, ref startOffset, "messageType"); @@ -142,6 +142,8 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return PingMessage.Instance; case HubProtocolConstants.CloseMessageType: return CreateCloseMessage(input, ref startOffset); + case HubProtocolConstants.StreamCompleteMessageType: + return CreateStreamCompleteMessage(input, ref startOffset); default: // Future protocol changes can add message types, old clients can ignore them return null; @@ -179,6 +181,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol var headers = ReadHeaders(input, ref offset); var invocationId = ReadInvocationId(input, ref offset); var target = ReadString(input, ref offset, "target"); + var parameterTypes = binder.GetParameterTypes(target); try @@ -196,7 +199,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol { var headers = ReadHeaders(input, ref offset); var invocationId = ReadInvocationId(input, ref offset); - var itemType = binder.GetReturnType(invocationId); + var itemType = binder.GetStreamItemType(invocationId); var value = DeserializeObject(input, ref offset, itemType, "item", resolver); return ApplyHeaders(headers, new StreamItemMessage(invocationId, value)); } @@ -244,6 +247,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new CloseMessage(error); } + private static StreamCompleteMessage CreateStreamCompleteMessage(byte[] input, ref int offset) + { + var streamId = ReadString(input, ref offset, "streamId"); + var error = ReadString(input, ref offset, "error"); + if (string.IsNullOrEmpty(error)) + { + error = null; + } + return new StreamCompleteMessage(streamId, error); + } + private static Dictionary ReadHeaders(byte[] input, ref int offset) { var headerCount = ReadMapLength(input, ref offset, "headers"); @@ -376,6 +390,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol case CloseMessage closeMessage: WriteCloseMessage(closeMessage, packer); break; + case StreamCompleteMessage m: + WriteStreamCompleteMessage(m, packer); + break; default: throw new InvalidDataException($"Unexpected message type: {message.GetType().Name}"); } @@ -469,6 +486,21 @@ namespace Microsoft.AspNetCore.SignalR.Protocol MessagePackBinary.WriteString(packer, message.InvocationId); } + private void WriteStreamCompleteMessage(StreamCompleteMessage message, Stream packer) + { + MessagePackBinary.WriteArrayHeader(packer, 3); + MessagePackBinary.WriteInt16(packer, HubProtocolConstants.StreamCompleteMessageType); + MessagePackBinary.WriteString(packer, message.StreamId); + if (message.HasError) + { + MessagePackBinary.WriteString(packer, message.Error); + } + else + { + MessagePackBinary.WriteNil(packer); + } + } + private void WriteCloseMessage(CloseMessage message, Stream packer) { MessagePackBinary.WriteArrayHeader(packer, 2); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index c95fd91df8..55bb728e3c 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -3,6 +3,9 @@ using System; using System.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.SignalR.Protocol; @@ -145,6 +148,227 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task StreamIntsToServer() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Trace)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: loggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("SomeMethod", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + Assert.Equal("SomeMethod", invocation["target"]); + var streamId = invocation["arguments"][0]["streamId"]; + + foreach (var number in new[] { 42, 43, 322, 3145, -1234 }) + { + await channel.Writer.WriteAsync(number).AsTask().OrTimeout(); + + var item = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamDataMessageType, item["type"]); + Assert.Equal(number, item["item"]); + Assert.Equal(streamId, item["streamId"]); + } + + channel.Writer.TryComplete(); + var completion = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamCompleteMessageType, completion["type"]); + + await connection.ReceiveJsonMessage( + new { type = HubProtocolConstants.CompletionMessageType, invocationId = invocation["invocationId"], result = 42 } + ).OrTimeout(); + var result = await invokeTask.OrTimeout(); + Assert.Equal(42, result); + } + } + + [Fact] + public async Task StreamIntsToServerViaSend() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Trace)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: loggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var sendTask = hubConnection.SendAsync("SomeMethod", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + Assert.Equal("SomeMethod", invocation["target"]); + Assert.Null(invocation["invocationId"]); + var streamId = invocation["arguments"][0]["streamId"]; + + foreach (var item in new[] { 2, 3, 10, 5 }) + { + await channel.Writer.WriteAsync(item); + + var received = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamDataMessageType, received["type"]); + Assert.Equal(item, received["item"]); + Assert.Equal(streamId, received["streamId"]); + } + } + } + + [Fact] + public async Task StreamsObjectsToServer() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Trace)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: loggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("UploadMethod", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + Assert.Equal("UploadMethod", invocation["target"]); + var id = invocation["invocationId"]; + + var items = new[] { new SampleObject("ab", 12), new SampleObject("ef", 23) }; + foreach (var item in items) + { + await channel.Writer.WriteAsync(item); + + var received = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamDataMessageType, received["type"]); + Assert.Equal(item.Foo, received["item"]["foo"]); + Assert.Equal(item.Bar, received["item"]["bar"]); + } + + channel.Writer.TryComplete(); + var completion = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamCompleteMessageType, completion["type"]); + + var expected = new SampleObject("oof", 14); + await connection.ReceiveJsonMessage( + new { type = HubProtocolConstants.CompletionMessageType, invocationId = id, result = expected } + ).OrTimeout(); + var result = await invokeTask.OrTimeout(); + + Assert.Equal(expected.Foo, result.Foo); + Assert.Equal(expected.Bar, result.Bar); + } + } + + [Fact] + public async Task UploadStreamCancelationSendsStreamComplete() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Trace)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: loggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var cts = new CancellationTokenSource(); + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("UploadMethod", channel.Reader, cts.Token); + + var invokeMessage = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invokeMessage["type"]); + + cts.Cancel(); + + // after cancellation, don't send from the pipe + foreach (var number in new[] { 42, 43, 322, 3145, -1234 }) + { + + await channel.Writer.WriteAsync(number); + } + + // the next sent message should be a completion message + var complete = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamCompleteMessageType, complete["type"]); + Assert.EndsWith("canceled by client.", ((string)complete["error"])); + } + } + + [Fact] + public async Task InvocationCanCompleteBeforeStreamCompletes() + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Trace)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: loggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("UploadMethod", channel.Reader); + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + var id = invocation["invocationId"]; + + await connection.ReceiveJsonMessage(new { type = HubProtocolConstants.CompletionMessageType, invocationId = id, result = 10 }); + + var result = await invokeTask.OrTimeout(); + Assert.Equal(10L, result); + + // after the server returns, with whatever response + // the client's behavior is undefined, and the server is responsible for ignoring stray messages + } + } + + [Fact] + public async Task WrongTypeOnServerResponse() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(HubConnection).FullName && + (writeContext.EventId.Name == "ServerDisconnectedWithError" + || writeContext.EventId.Name == "ShutdownWithError"); + } + using (StartVerifiableLog(out var loggerFactory, LogLevel.Trace, expectedErrorsFilter: ExpectedErrors)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: loggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + // we expect to get sent ints, and receive an int back + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("SumInts", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + var id = invocation["invocationId"]; + + await channel.Writer.WriteAsync(5); + await channel.Writer.WriteAsync(10); + + await connection.ReceiveJsonMessage(new { type = HubProtocolConstants.CompletionMessageType, invocationId = id, result = "humbug" }); + + try + { + await invokeTask; + } + catch (Exception ex) + { + Assert.Equal(typeof(Newtonsoft.Json.JsonSerializationException), ex.GetType()); + } + } + } + + private class SampleObject + { + public SampleObject(string foo, int bar) + { + Foo = foo; + Bar = bar; + } + + public string Foo { get; private set; } + public int Bar { get; private set; } + } + + // Moq really doesn't handle out parameters well, so to make these tests work I added a manual mock -anurse private class MockHubProtocol : IHubProtocol { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs index 7309fdee70..432d4595a8 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs @@ -163,6 +163,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + public async Task ReadSentJsonAsync() + { + return JObject.Parse(await ReadSentTextMessageAsync()); + } + public async Task> ReadAllSentMessagesAsync(bool ignorePings = true) { if (!Disposed.IsCompleted) diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs index b1957dc5fa..06354680a4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs @@ -37,5 +37,10 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol arg is StreamItemMessage || arg is StreamInvocationMessage; } + + public Type GetStreamItemType(string streamId) + { + throw new NotImplementedException(); + } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index 8229dac321..7bd1946100 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("InvocationMessage_HasStreamPlaceholder", new InvocationMessage(null, "Target", new object[] { new StreamPlaceholder("__test_id__")}), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"streamId\":\"__test_id__\"}]}"), new JsonProtocolTestData("InvocationMessage_HasHeaders", AddHeaders(TestHeaders, new InvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f })), true, NullValueHandling.Ignore, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"), new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgument", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Include, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, NullValueHandling.Include, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasIntergerResult", CompletionMessage.WithResult("123", 1), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"), + new JsonProtocolTestData("CompletionMessage_HasIntegerResult", CompletionMessage.WithResult("123", 1), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"), new JsonProtocolTestData("CompletionMessage_HasStringResult", CompletionMessage.WithResult("123", "Foo"), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}"), new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}"), new JsonProtocolTestData("CompletionMessage_HasBoolResult", CompletionMessage.WithResult("123", true), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":true}"), @@ -88,7 +89,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("CloseMessage", CloseMessage.Empty, false, NullValueHandling.Ignore, "{\"type\":7}"), new JsonProtocolTestData("CloseMessage_HasError", new CloseMessage("Error!"), false, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"Error!\"}"), new JsonProtocolTestData("CloseMessage_HasErrorWithCamelCase", new CloseMessage("Error!"), true, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"Error!\"}"), - new JsonProtocolTestData("CloseMessage_HasErrorEmptyString", new CloseMessage(""), false, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"\"}") + new JsonProtocolTestData("CloseMessage_HasErrorEmptyString", new CloseMessage(""), false, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"\"}"), + + new JsonProtocolTestData("StreamCompleteMessage", new StreamCompleteMessage("123"), true, NullValueHandling.Ignore, "{\"type\":8,\"streamId\":\"123\"}"), + new JsonProtocolTestData("StreamCompleteMessageWithError", new StreamCompleteMessage("123", "zoinks"), true, NullValueHandling.Ignore, "{\"type\":8,\"streamId\":\"123\",\"error\":\"zoinks\"}"), + }.ToDictionary(t => t.Name); public static IEnumerable ProtocolTestDataNames => ProtocolTestData.Keys.Select(name => new object[] { name }); @@ -101,6 +106,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("StreamInvocationMessage_IntegerArrayArgumentFirst", new StreamInvocationMessage("3", "Method", new object[] { 1, 2 }), false, NullValueHandling.Ignore, "{ \"type\":4, \"arguments\": [1,2], \"target\": \"Method\", \"invocationId\": \"3\" }"), new JsonProtocolTestData("CompletionMessage_ResultFirst", new CompletionMessage("15", null, 10, hasResult: true), false, NullValueHandling.Ignore, "{ \"type\":3, \"result\": 10, \"invocationId\": \"15\" }"), new JsonProtocolTestData("StreamItemMessage_ItemFirst", new StreamItemMessage("1a", "foo"), false, NullValueHandling.Ignore, "{ \"item\": \"foo\", \"invocationId\": \"1a\", \"type\":2 }") + }.ToDictionary(t => t.Name); public static IEnumerable OutOfOrderJsonTestDataNames => OutOfOrderJsonTestData.Keys.Select(name => new object[] { name }); diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index c0fbec1baf..1f8bf8c009 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -91,6 +91,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs", message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })), binary: "lQGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="), + new ProtocolTestData( + name: "InvocationWithStreamPlaceholderObject", + message: new InvocationMessage(null, "Target", new object[] { new StreamPlaceholder("__test_id__")}), + binary: "lQGAwKZUYXJnZXSRgahTdHJlYW1JZKtfX3Rlc3RfaWRfXw==" + ), // StreamItem Messages new ProtocolTestData( @@ -228,6 +233,16 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")), binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"), + // StreamComplete Messages + new ProtocolTestData( + name: "StreamComplete", + message: new StreamCompleteMessage("xyz"), + binary: "kwijeHl6wA=="), + new ProtocolTestData( + name: "StreamCompleteWithError", + message: new StreamCompleteMessage("xyz", "zoinks"), + binary: "kwijeHl6pnpvaW5rcw=="), + // Ping Messages new ProtocolTestData( name: "Ping", @@ -259,7 +274,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty()); // Verify that the input binary string decodes to the expected MsgPack primitives - var bytes = new byte[] { ArrayBytes(6), 1, 0x80, StringBytes(3), (byte)'x', (byte)'y', (byte)'z', StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d', ArrayBytes(0), StringBytes(2), (byte)'e', (byte)'x' }; + var bytes = new byte[] { ArrayBytes(8), + 1, + 0x80, + StringBytes(3), (byte)'x', (byte)'y', (byte)'z', + StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d', + ArrayBytes(0), + 0xc3, + StringBytes(2), (byte)'e', (byte)'x' }; // Parse the input fully now. bytes = Frame(bytes); diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs index 1855b19f81..d7282c42a1 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs @@ -58,5 +58,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol } throw new InvalidOperationException("Unexpected binder call"); } + + public Type GetStreamItemType(string streamId) + { + // In v1, stream items were only sent from server -> client + // and so they had items typed based on what the hub method returned. + // We just forward here for backwards compatibility. + return GetReturnType(streamId); + } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs index a7c425be0a..0682c75640 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs @@ -34,11 +34,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol return StreamInvocationMessagesEqual(streamInvocationMessage, (StreamInvocationMessage)y); case CancelInvocationMessage cancelItemMessage: return string.Equals(cancelItemMessage.InvocationId, ((CancelInvocationMessage)y).InvocationId, StringComparison.Ordinal); - case PingMessage pingMessage: + case PingMessage _: // If the types are equal (above), then we're done. return true; case CloseMessage closeMessage: return string.Equals(closeMessage.Error, ((CloseMessage) y).Error); + case StreamCompleteMessage streamCompleteMessage: + return StreamCompleteMessagesEqual(streamCompleteMessage, (StreamCompleteMessage)y); default: throw new InvalidOperationException($"Unknown message type: {x.GetType().FullName}"); } @@ -46,34 +48,40 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol private bool CompletionMessagesEqual(CompletionMessage x, CompletionMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - string.Equals(x.Error, y.Error, StringComparison.Ordinal) && - x.HasResult == y.HasResult && - (Equals(x.Result, y.Result) || SequenceEqual(x.Result, y.Result)); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && string.Equals(x.Error, y.Error, StringComparison.Ordinal) + && x.HasResult == y.HasResult + && (Equals(x.Result, y.Result) || SequenceEqual(x.Result, y.Result)); } private bool StreamItemMessagesEqual(StreamItemMessage x, StreamItemMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - (Equals(x.Item, y.Item) || SequenceEqual(x.Item, y.Item)); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && (Equals(x.Item, y.Item) || SequenceEqual(x.Item, y.Item)); } private bool InvocationMessagesEqual(InvocationMessage x, InvocationMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - string.Equals(x.Target, y.Target, StringComparison.Ordinal) && - ArgumentListsEqual(x.Arguments, y.Arguments); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && string.Equals(x.Target, y.Target, StringComparison.Ordinal) + && ArgumentListsEqual(x.Arguments, y.Arguments); } private bool StreamInvocationMessagesEqual(StreamInvocationMessage x, StreamInvocationMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - string.Equals(x.Target, y.Target, StringComparison.Ordinal) && - ArgumentListsEqual(x.Arguments, y.Arguments); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && string.Equals(x.Target, y.Target, StringComparison.Ordinal) + && ArgumentListsEqual(x.Arguments, y.Arguments); + } + + private bool StreamCompleteMessagesEqual(StreamCompleteMessage x, StreamCompleteMessage y) + { + return x.StreamId == y.StreamId + && y.Error == y.Error; } private bool ArgumentListsEqual(object[] left, object[] right) @@ -90,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol for (var i = 0; i < left.Length; i++) { - if (!(Equals(left[i], right[i]) || SequenceEqual(left[i], right[i]))) + if (!(Equals(left[i], right[i]) || SequenceEqual(left[i], right[i]) || PlaceholdersEqual(left[i], right[i]))) { return false; } @@ -98,6 +106,21 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol return true; } + private bool PlaceholdersEqual(object left, object right) + { + if (left.GetType() != right.GetType()) + { + return false; + } + switch(left) + { + case StreamPlaceholder leftPlaceholder: + return leftPlaceholder.StreamId == (right as StreamPlaceholder).StreamId; + default: + return false; + } + } + private bool SequenceEqual(object left, object right) { if (left == null && right == null) diff --git a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs index c76ef5e892..947a323b55 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs @@ -178,6 +178,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests return SendHubMessageAsync(new StreamInvocationMessage(invocationId, methodName, args)); } + public Task BeginUploadStreamAsync(string invocationId, string methodName, params object[] args) + { + var message = new InvocationMessage(invocationId, methodName, args); + return SendHubMessageAsync(message); + } + public async Task SendHubMessageAsync(HubMessage message) { var payload = _protocol.GetMessageBytes(message); @@ -295,6 +301,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } } + private class DefaultInvocationBinder : IInvocationBinder { public IReadOnlyList GetParameterTypes(string methodName) @@ -307,6 +314,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests { return typeof(object); } + + public Type GetStreamItemType(string streamId) + { + throw new NotImplementedException(); + } } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs index 7eb0aec498..c1d30c5cbc 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -174,6 +175,78 @@ namespace Microsoft.AspNetCore.SignalR.Tests public SelfRef Self; } + + public async Task StreamingConcat(ChannelReader source) + { + var sb = new StringBuilder(); + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + sb.Append(item); + } + } + + return sb.ToString(); + } + + public async Task StreamingSum(ChannelReader source) + { + var total = 0; + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + total += item; + } + } + return total; + } + + public async Task> UploadArray(ChannelReader source) + { + var results = new List(); + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + results.Add(item); + } + } + + return results; + } + + public async Task TestTypeCastingErrors(ChannelReader source) + { + try + { + await source.WaitToReadAsync(); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return "error identified and caught"; + } + + return "wrong type accepted, this is bad"; + } + + public async Task TestCustomErrorPassing(ChannelReader source) + { + try + { + await source.WaitToReadAsync(); + } + catch (Exception ex) + { + return ex.Message == HubConnectionHandlerTests.CustomErrorMessage; + } + + return false; + } } public abstract class TestHub : Hub diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs index 04156ad03b..5f01aad337 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Security.Claims; using System.Text; @@ -1489,7 +1490,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); var connectionHandler = serviceProvider.GetService>(); var invocationBinder = new Mock(); - invocationBinder.Setup(b => b.GetReturnType(It.IsAny())).Returns(typeof(string)); + invocationBinder.Setup(b => b.GetStreamItemType(It.IsAny())).Returns(typeof(string)); using (var client = new TestClient(protocol: protocol, invocationBinder: invocationBinder.Object)) { @@ -2361,6 +2362,93 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task UploadStringsToConcat() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.StreamingConcat), new StreamPlaceholder("id")); + + foreach (var letter in new[] { "B", "E", "A", "N", "E", "D" }) + { + await client.SendHubMessageAsync(new StreamDataMessage("id", letter)).OrTimeout(); + } + + await client.SendHubMessageAsync(new StreamCompleteMessage("id")).OrTimeout(); + var result = (CompletionMessage)await client.ReadAsync().OrTimeout(); + + Assert.Equal("BEANED", result.Result); + } + } + + [Fact] + public async Task UploadStreamedObjects() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.UploadArray), new StreamPlaceholder("id")); + + var objects = new[] { new SampleObject("solo", 322), new SampleObject("ggez", 3145) }; + foreach (var thing in objects) + { + await client.SendHubMessageAsync(new StreamDataMessage("id", thing)).OrTimeout(); + } + + await client.SendHubMessageAsync(new StreamCompleteMessage("id")).OrTimeout(); + var response = (CompletionMessage)await client.ReadAsync().OrTimeout(); + var result = ((JArray)response.Result).ToArray(); + + Assert.Equal(objects[0].Foo, ((JContainer)result[0])["foo"]); + Assert.Equal(objects[0].Bar, ((JContainer)result[0])["bar"]); + Assert.Equal(objects[1].Foo, ((JContainer)result[1])["foo"]); + Assert.Equal(objects[1].Bar, ((JContainer)result[1])["bar"]); + } + } + + [Fact] + public async Task UploadManyStreams() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + var ids = new[] { "0", "1", "2" }; + + foreach (string id in ids) + { + await client.BeginUploadStreamAsync("invocation_"+id, nameof(MethodHub.StreamingConcat), new StreamPlaceholder(id)); + } + + var words = new[] { "zygapophyses", "qwerty", "abcd" }; + var pos = new[] { 0, 0, 0 }; + var order = new[] { 2, 2, 0, 2, 1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1 }; + + foreach (var spot in order) + { + await client.SendHubMessageAsync(new StreamDataMessage(spot.ToString(), words[spot][pos[spot]])).OrTimeout(); + pos[spot] += 1; + } + + foreach (string id in new[] { "0", "2", "1" }) + { + await client.SendHubMessageAsync(new StreamCompleteMessage(id)).OrTimeout(); + var response = await client.ReadAsync().OrTimeout(); + Debug.Write(response); + Assert.Equal(words[int.Parse(id)], ((CompletionMessage)response).Result); + } + } + } + [Fact] public async Task ConnectionAbortedIfSendFailsWithProtocolError() { @@ -2381,6 +2469,30 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task UploadStreamItemInvalidTypeAutoCasts() + { + // NOTE -- json.net is flexible here, and casts for us + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.StreamingConcat), new StreamPlaceholder("id")).OrTimeout(); + + // send integers that are then cast to strings + await client.SendHubMessageAsync(new StreamDataMessage("id", 5)).OrTimeout(); + await client.SendHubMessageAsync(new StreamDataMessage("id", 10)).OrTimeout(); + + await client.SendHubMessageAsync(new StreamCompleteMessage("id")).OrTimeout(); + var response = (CompletionMessage)await client.ReadAsync().OrTimeout(); + + Assert.Equal("510", response.Result); + } + } + [Fact] public async Task ServerReportsProtocolMinorVersion() { @@ -2395,7 +2507,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests using (var client = new TestClient(protocol: testProtocol.Object)) { - var connectionHandlerTask = await client.ConnectAsync(connectionHandler); + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); Assert.NotNull(client.HandshakeResponseMessage); Assert.Equal(112, client.HandshakeResponseMessage.MinorVersion); @@ -2405,6 +2517,89 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task UploadStreamItemInvalidType() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocationId", nameof(MethodHub.TestTypeCastingErrors), new StreamPlaceholder("channelId")).OrTimeout(); + + // client is running wild, sending strings not ints. + // this error should be propogated to the user's HubMethod code + await client.SendHubMessageAsync(new StreamItemMessage("channelId", "not a number")).OrTimeout(); + var response = await client.ReadAsync().OrTimeout(); + + Assert.Equal(typeof(CompletionMessage), response.GetType()); + Assert.Equal("error identified and caught", (string)((CompletionMessage)response).Result); + } + } + + [Fact] + public async Task UploadStreamItemInvalidId() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => + { + services.AddSignalR(options => options.EnableDetailedErrors = true); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.SendHubMessageAsync(new StreamItemMessage("fake_id", "not a number")).OrTimeout(); + + // Client is breaking protocol by sending an invalid id, and should be closed. + var message = client.TryRead(); + Assert.IsType(message); + Assert.Equal("Connection closed with an error. KeyNotFoundException: No stream with id 'fake_id' could be found.", ((CloseMessage)message).Error); + } + } + + [Fact] + public async Task UploadStreamCompleteInvalidId() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => + { + services.AddSignalR(options => options.EnableDetailedErrors = true); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.SendHubMessageAsync(new StreamCompleteMessage("fake_id")).OrTimeout(); + + // Client is breaking protocol by sending an invalid id, and should be closed. + var message = client.TryRead(); + Assert.IsType(message); + Assert.Equal("Connection closed with an error. KeyNotFoundException: No stream with id 'fake_id' could be found.", ((CloseMessage)message).Error); + } + } + + public static string CustomErrorMessage = "custom error for testing ::::)"; + + [Fact] + public async Task UploadStreamCompleteWithError() + { + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.TestCustomErrorPassing), new StreamPlaceholder("id")).OrTimeout(); + await client.SendHubMessageAsync(new StreamCompleteMessage("id", CustomErrorMessage)).OrTimeout(); + + var response = (CompletionMessage)await client.ReadAsync().OrTimeout(); + Assert.True((bool)response.Result); + } + } + private class CustomHubActivator : IHubActivator where THub : Hub { public int ReleaseCount; @@ -2450,5 +2645,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests public string GetUserId(HubConnectionContext connection) => _getUserId(connection); } + + private class SampleObject + { + public SampleObject(string foo, int bar) + { + Bar = bar; + Foo = foo; + } + public int Bar { get; } + public string Foo { get; } + } } }