Merge branch 'release/2.2'

This commit is contained in:
BrennanConroy 2018-09-12 09:29:51 -07:00
commit b423313e5c
56 changed files with 856 additions and 9343 deletions

4
.vscode/launch.json vendored
View File

@ -21,7 +21,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Jest - All", "name": "Jest - All",
"program": "${workspaceFolder}/clients/ts/node_modules/jest/bin/jest", "program": "${workspaceFolder}/clients/ts/common/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/clients/ts", "cwd": "${workspaceFolder}/clients/ts",
"args": ["--runInBand"], "args": ["--runInBand"],
"console": "integratedTerminal", "console": "integratedTerminal",
@ -31,7 +31,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Jest - Current File", "name": "Jest - Current File",
"program": "${workspaceFolder}/clients/ts/node_modules/jest/bin/jest", "program": "${workspaceFolder}/clients/ts/common/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/clients/ts", "cwd": "${workspaceFolder}/clients/ts",
"args": ["${relativeFile}"], "args": ["${relativeFile}"],
"console": "integratedTerminal", "console": "integratedTerminal",

View File

@ -8,7 +8,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class CallbackMap { class CallbackMap {
private ConcurrentHashMap<String, List<ActionBase>> handlers = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, List<ActionBase>> handlers = new ConcurrentHashMap<>();
public void put(String target, ActionBase action) { public void put(String target, ActionBase action) {

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public class CloseMessage extends HubMessage { class CloseMessage extends HubMessage {
String error; String error;
@Override @Override

View File

@ -5,7 +5,7 @@ package com.microsoft.aspnet.signalr;
import com.google.gson.Gson; import com.google.gson.Gson;
public class HandshakeProtocol { class HandshakeProtocol {
public static Gson gson = new Gson(); public static Gson gson = new Gson();
private static final String RECORD_SEPARATOR = "\u001e"; private static final String RECORD_SEPARATOR = "\u001e";

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public class HandshakeRequestMessage { class HandshakeRequestMessage {
String protocol; String protocol;
int version; int version;

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public class HandshakeResponseMessage { class HandshakeResponseMessage {
public String error; public String error;
public HandshakeResponseMessage() { public HandshakeResponseMessage() {

View File

@ -6,6 +6,6 @@ package com.microsoft.aspnet.signalr;
/** /**
* A base class for hub messages. * A base class for hub messages.
*/ */
public abstract class HubMessage { abstract class HubMessage {
public abstract HubMessageType getMessageType(); public abstract HubMessageType getMessageType();
} }

View File

@ -6,7 +6,7 @@ package com.microsoft.aspnet.signalr;
/** /**
* A protocol abstraction for communicating with SignalR hubs. * A protocol abstraction for communicating with SignalR hubs.
*/ */
public interface HubProtocol { interface HubProtocol {
String getName(); String getName();
int getVersion(); int getVersion();
TransferFormat getTransferFormat(); TransferFormat getTransferFormat();

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public class InvocationMessage extends HubMessage { class InvocationMessage extends HubMessage {
int type = HubMessageType.INVOCATION.value; int type = HubMessageType.INVOCATION.value;
String invocationId; String invocationId;
String target; String target;

View File

@ -11,7 +11,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
public class JsonHubProtocol implements HubProtocol { class JsonHubProtocol implements HubProtocol {
private final JsonParser jsonParser = new JsonParser(); private final JsonParser jsonParser = new JsonParser();
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private static final String RECORD_SEPARATOR = "\u001e"; private static final String RECORD_SEPARATOR = "\u001e";

View File

@ -10,7 +10,7 @@ import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
public class Negotiate { class Negotiate {
public static NegotiateResponse processNegotiate(String url) throws IOException { public static NegotiateResponse processNegotiate(String url) throws IOException {
return processNegotiate(url, null); return processNegotiate(url, null);

View File

@ -10,7 +10,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
public class NegotiateResponse { class NegotiateResponse {
private String connectionId; private String connectionId;
private Set<String> availableTransports = new HashSet<>(); private Set<String> availableTransports = new HashSet<>();
private String redirectUrl; private String redirectUrl;

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public class NullLogger implements Logger { class NullLogger implements Logger {
@Override @Override
public void log(LogLevel logLevel, String message) { } public void log(LogLevel logLevel, String message) { }

View File

@ -3,6 +3,6 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public interface OnReceiveCallBack { interface OnReceiveCallBack {
void invoke(String message) throws Exception; void invoke(String message) throws Exception;
} }

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public class PingMessage extends HubMessage { class PingMessage extends HubMessage {
int type = HubMessageType.PING.value; int type = HubMessageType.PING.value;

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr; package com.microsoft.aspnet.signalr;
public interface Transport { interface Transport {
void start() throws Exception; void start() throws Exception;
void send(String message) throws Exception; void send(String message) throws Exception;
void setOnReceive(OnReceiveCallBack callback); void setOnReceive(OnReceiveCallBack callback);

View File

@ -10,7 +10,7 @@ import java.util.Map;
import org.java_websocket.client.WebSocketClient; import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake; import org.java_websocket.handshake.ServerHandshake;
public class WebSocketTransport implements Transport { class WebSocketTransport implements Transport {
private WebSocketClient webSocketClient; private WebSocketClient webSocketClient;
private OnReceiveCallBack onReceiveCallBack; private OnReceiveCallBack onReceiveCallBack;
private URI url; private URI url;

View File

@ -1,16 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.Test; import org.junit.Test;
import com.microsoft.aspnet.signalr.HandshakeProtocol;
import com.microsoft.aspnet.signalr.HandshakeRequestMessage;
import com.microsoft.aspnet.signalr.HandshakeResponseMessage;
public class HandshakeProtocolTest { public class HandshakeProtocolTest {
@Test @Test

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -12,7 +12,6 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import com.microsoft.aspnet.signalr.*;
public class HubConnectionTest { public class HubConnectionTest {
private static final String RECORD_SEPARATOR = "\u001e"; private static final String RECORD_SEPARATOR = "\u001e";

View File

@ -1,14 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import org.junit.Test; import org.junit.Test;
import com.microsoft.aspnet.signalr.HubException;
public class HubExceptionTest { public class HubExceptionTest {
@Test @Test
public void VeryHubExceptionMesssageIsSet() { public void VeryHubExceptionMesssageIsSet() {

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -10,7 +10,6 @@ import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.microsoft.aspnet.signalr.*;
public class JsonHubProtocolTest { public class JsonHubProtocolTest {
private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol(); private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol();

View File

@ -1,13 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.Test; import org.junit.Test;
import com.microsoft.aspnet.signalr.NegotiateResponse;
public class NegotiateResponseTest { public class NegotiateResponseTest {

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -12,7 +12,6 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import com.microsoft.aspnet.signalr.Negotiate;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class ResolveNegotiateUrlTest { public class ResolveNegotiateUrlTest {

View File

@ -1,16 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import com.microsoft.aspnet.signalr.NullLogger;
import com.microsoft.aspnet.signalr.Transport;
import com.microsoft.aspnet.signalr.WebSocketTransport;
public class WebSocketTransportTest { public class WebSocketTransportTest {
@Rule @Rule

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test; package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -13,8 +13,6 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import com.microsoft.aspnet.signalr.NullLogger;
import com.microsoft.aspnet.signalr.WebSocketTransport;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class WebSocketTransportUrlFormatTest { public class WebSocketTransportUrlFormatTest {

View File

@ -0,0 +1,40 @@
// 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.
module.exports = {
transformIgnorePatterns: [
// We reference the ESM output from tests and don't want to run them through jest as it won't understand the syntax
".*/node_modules/(?!@aspnet)"
],
globals: {
"ts-jest": {
"tsConfigFile": "../tsconfig.jest.json",
"skipBabel": true,
// Needed in order to properly process the JS files
// We run 'tsc --noEmit' to get TS diagnostics before the test instead
"enableTsDiagnostics": false,
}
},
reporters: [
"default",
["../common/node_modules/jest-junit/index.js", { "output": "../../../artifacts/logs/" + `${process.platform}` + ".node.functional.junit.xml" }]
],
transform: {
"^.+\\.(jsx?|tsx?)$": "../common/node_modules/ts-jest"
},
testEnvironment: "node",
testRegex: "(Tests)\\.(jsx?|tsx?)$",
moduleNameMapper: {
"^ts-jest$": "<rootDir>/../common/node_modules/ts-jest",
"^@aspnet/signalr$": "<rootDir>/../signalr/dist/cjs/index.js"
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
};

View File

@ -6,14 +6,88 @@
"dependencies": { "dependencies": {
"@aspnet/signalr": { "@aspnet/signalr": {
"version": "file:../signalr", "version": "file:../signalr",
"requires": {
"eventsource": "^1.0.7",
"websocket": "^1.0.26"
},
"dependencies": { "dependencies": {
"debug": {
"version": "2.6.9",
"bundled": true,
"requires": {
"ms": "2.0.0"
}
},
"es6-promise": { "es6-promise": {
"version": "4.2.2", "version": "4.2.2",
"bundled": true "bundled": true
}, },
"eventsource": {
"version": "1.0.7",
"bundled": true,
"requires": {
"original": "^1.0.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"bundled": true
},
"ms": {
"version": "2.0.0",
"bundled": true
},
"nan": {
"version": "2.11.0",
"bundled": true
},
"original": {
"version": "1.0.2",
"bundled": true,
"requires": {
"url-parse": "^1.4.3"
}
},
"querystringify": {
"version": "2.0.0",
"bundled": true
},
"requires-port": {
"version": "1.0.0",
"bundled": true
},
"tslib": { "tslib": {
"version": "1.9.3", "version": "1.9.3",
"bundled": true "bundled": true
},
"typedarray-to-buffer": {
"version": "3.1.5",
"bundled": true,
"requires": {
"is-typedarray": "^1.0.0"
}
},
"url-parse": {
"version": "1.4.3",
"bundled": true,
"requires": {
"querystringify": "^2.0.0",
"requires-port": "^1.0.0"
}
},
"websocket": {
"version": "1.0.26",
"bundled": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
"typedarray-to-buffer": "^3.1.2",
"yaeti": "^0.0.6"
}
},
"yaeti": {
"version": "0.0.6",
"bundled": true
} }
} }
}, },
@ -1449,12 +1523,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -1469,17 +1545,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -1596,7 +1675,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -1608,6 +1688,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -1622,6 +1703,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -1629,12 +1711,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -1653,6 +1737,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -1733,7 +1818,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -1745,6 +1831,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -1866,6 +1953,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -2711,8 +2799,7 @@
"version": "2.10.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true, "dev": true
"optional": true
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
@ -3623,6 +3710,15 @@
"mime-types": "~2.1.18" "mime-types": "~2.1.18"
} }
}, },
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
},
"typescript": { "typescript": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz",
@ -3922,6 +4018,29 @@
} }
} }
}, },
"websocket": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz",
"integrity": "sha512-fjcrYDPIQxpTnqFQ9JjxUQcdvR89MFAOjPBlF+vjOt49w/XW4fJknUoMz/mDIn2eK1AdslVojcaOxOqyZZV8rw==",
"dev": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
"typedarray-to-buffer": "^3.1.2",
"yaeti": "^0.0.6"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"which": { "which": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -3972,6 +4091,12 @@
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true "dev": true
}, },
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
"dev": true
},
"yeast": { "yeast": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",

View File

@ -31,7 +31,8 @@
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-summary-reporter": "^1.5.0", "karma-summary-reporter": "^1.5.0",
"ts-node": "^4.1.0", "ts-node": "^4.1.0",
"typescript": "^3.0.1" "typescript": "^3.0.1",
"websocket": " ^1.0.26"
}, },
"scripts": { "scripts": {
"clean": "node ../common/node_modules/rimraf/bin.js ./wwwroot/dist ./obj/js", "clean": "node ../common/node_modules/rimraf/bin.js ./wwwroot/dist ./obj/js",
@ -40,7 +41,7 @@
"build:webpack": "node ../common/node_modules/webpack-cli/bin/cli.js", "build:webpack": "node ../common/node_modules/webpack-cli/bin/cli.js",
"build:parent": "cd .. && npm run build", "build:parent": "cd .. && npm run build",
"pretest": "npm run build:parent && npm run build && dotnet build --no-restore", "pretest": "npm run build:parent && npm run build && dotnet build --no-restore",
"test": "npm run test:local --", "test": "tsc --noEmit && npm run test:local --",
"test:inner": "npm run build && dotnet build && npm run test:local --", "test:inner": "npm run build && dotnet build && npm run test:local --",
"test:local": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts", "test:local": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts",
"test:all": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts --all-browsers", "test:all": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts --all-browsers",

View File

@ -1,4 +1,4 @@
import { ChildProcess, spawn } from "child_process"; import { ChildProcess, execSync, spawn } from "child_process";
import { EOL } from "os"; import { EOL } from "os";
import { Readable } from "stream"; import { Readable } from "stream";
@ -9,6 +9,7 @@ import { promisify } from "util";
import * as karma from "karma"; import * as karma from "karma";
import * as _debug from "debug"; import * as _debug from "debug";
const debug = _debug("signalr-functional-tests:run"); const debug = _debug("signalr-functional-tests:run");
const ARTIFACTS_DIR = path.resolve(__dirname, "..", "..", "..", "..", "artifacts"); const ARTIFACTS_DIR = path.resolve(__dirname, "..", "..", "..", "..", "artifacts");
@ -156,6 +157,22 @@ function runKarma(karmaConfig) {
}); });
} }
function runJest(url: string) {
const jestPath = path.resolve(__dirname, "..", "..", "common", "node_modules", "jest", "bin", "jest.js");
const configPath = path.resolve(__dirname, "..", "func.jest.config.js");
console.log("Starting Node tests using Jest.");
try {
execSync(`"${process.execPath}" "${jestPath}" --config "${configPath}"`, { env: { SERVER_URL: url }, timeout: 200000 });
return 0;
} catch (error) {
console.log(error.message);
console.log(error.stderr);
console.log(error.stdout);
return error.status;
}
}
(async () => { (async () => {
try { try {
// Check if we got any browsers // Check if we got any browsers
@ -223,7 +240,12 @@ function runKarma(karmaConfig) {
conf.client.args = ["--server", url]; conf.client.args = ["--server", url];
const results = await runKarma(conf); const results = await runKarma(conf);
process.exit(results.exitCode);
const jestExit = runJest(url);
console.log(`karma exit code: ${results.exitCode}`);
console.log(`jest exit code: ${jestExit}`);
process.exit(results.exitCode !== 0 ? results.exitCode : jestExit);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
process.exit(1); process.exit(1);

View File

@ -6,7 +6,7 @@ import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
export let ENDPOINT_BASE_URL: string; export let ENDPOINT_BASE_URL: string;
if ((window as any).__karma__) { if (typeof window !== "undefined" && (window as any).__karma__) {
const args = (window as any).__karma__.config.args as string[]; const args = (window as any).__karma__.config.args as string[];
let server = ""; let server = "";
@ -22,19 +22,28 @@ if ((window as any).__karma__) {
// Running in Karma? Need to use an absolute URL // Running in Karma? Need to use an absolute URL
ENDPOINT_BASE_URL = server; ENDPOINT_BASE_URL = server;
console.log(`Using SignalR Server: ${ENDPOINT_BASE_URL}`); console.log(`Using SignalR Server: ${ENDPOINT_BASE_URL}`);
} else { } else if (typeof document !== "undefined") {
ENDPOINT_BASE_URL = `${document.location.protocol}//${document.location.host}`; ENDPOINT_BASE_URL = `${document.location.protocol}//${document.location.host}`;
} else if (process && process.env && process.env.SERVER_URL) {
ENDPOINT_BASE_URL = process.env.SERVER_URL;
} else {
throw new Error("The server could not be found.");
} }
export const ECHOENDPOINT_URL = ENDPOINT_BASE_URL + "/echo"; export const ECHOENDPOINT_URL = ENDPOINT_BASE_URL + "/echo";
export function getHttpTransportTypes(): HttpTransportType[] { export function getHttpTransportTypes(): HttpTransportType[] {
const transportTypes = []; const transportTypes = [];
if (typeof WebSocket !== "undefined") { if (typeof window === "undefined") {
transportTypes.push(HttpTransportType.WebSockets); transportTypes.push(HttpTransportType.WebSockets);
}
if (typeof EventSource !== "undefined") {
transportTypes.push(HttpTransportType.ServerSentEvents); transportTypes.push(HttpTransportType.ServerSentEvents);
} else {
if (typeof WebSocket !== "undefined") {
transportTypes.push(HttpTransportType.WebSockets);
}
if (typeof EventSource !== "undefined") {
transportTypes.push(HttpTransportType.ServerSentEvents);
}
} }
transportTypes.push(HttpTransportType.LongPolling); transportTypes.push(HttpTransportType.LongPolling);
@ -49,9 +58,8 @@ export function eachTransport(action: (transport: HttpTransportType) => void) {
export function eachTransportAndProtocol(action: (transport: HttpTransportType, protocol: IHubProtocol) => void) { export function eachTransportAndProtocol(action: (transport: HttpTransportType, protocol: IHubProtocol) => void) {
const protocols: IHubProtocol[] = [new JsonHubProtocol()]; const protocols: IHubProtocol[] = [new JsonHubProtocol()];
// IE9 does not support XmlHttpRequest advanced features so disable for now // Run messagepack tests in Node and Browsers that support binary content (indicated by the presence of responseType property)
// This can be enabled if we fix: https://github.com/aspnet/SignalR/issues/742 if (typeof XMLHttpRequest === "undefined" || typeof new XMLHttpRequest().responseType === "string") {
if (typeof new XMLHttpRequest().responseType === "string") {
// Because of TypeScript stuff, we can't get "ambient" or "global" declarations to work with the MessagePackHubProtocol module // Because of TypeScript stuff, we can't get "ambient" or "global" declarations to work with the MessagePackHubProtocol module
// This is only a limitation of the .d.ts file. // This is only a limitation of the .d.ts file.
// Everything works fine in the module // Everything works fine in the module
@ -65,3 +73,7 @@ export function eachTransportAndProtocol(action: (transport: HttpTransportType,
}); });
}); });
} }
export function getGlobalObject(): any {
return typeof window !== "undefined" ? window : global;
}

View File

@ -4,7 +4,7 @@
// This code uses a lot of `.then` instead of `await` and TSLint doesn't like it. // This code uses a lot of `.then` instead of `await` and TSLint doesn't like it.
// tslint:disable:no-floating-promises // tslint:disable:no-floating-promises
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol } from "@aspnet/signalr"; import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@aspnet/signalr";
import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack"; import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_URL } from "./Common"; import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_URL } from "./Common";
@ -604,6 +604,12 @@ describe("hubConnection", () => {
}); });
it("transport falls back from WebSockets to SSE or LongPolling", async (done) => { it("transport falls back from WebSockets to SSE or LongPolling", async (done) => {
// Skip test on Node as there will always be a WebSockets implementation on Node
if (typeof window === "undefined") {
done();
return;
}
// Replace Websockets with a function that just // Replace Websockets with a function that just
// throws to force fallback. // throws to force fallback.
const oldWebSocket = (window as any).WebSocket; const oldWebSocket = (window as any).WebSocket;
@ -683,6 +689,11 @@ describe("hubConnection", () => {
}); });
it("populates the Content-Type header when sending XMLHttpRequest", async (done) => { it("populates the Content-Type header when sending XMLHttpRequest", async (done) => {
// Skip test on Node as this header isn't set (it was added for React-Native)
if (typeof window === "undefined") {
done();
return;
}
const hubConnection = getConnectionBuilder(HttpTransportType.LongPolling, TESTHUB_NOWEBSOCKETS_ENDPOINT_URL) const hubConnection = getConnectionBuilder(HttpTransportType.LongPolling, TESTHUB_NOWEBSOCKETS_ENDPOINT_URL)
.withHubProtocol(new JsonHubProtocol()) .withHubProtocol(new JsonHubProtocol())
.build(); .build();
@ -704,22 +715,14 @@ describe("hubConnection", () => {
function getJwtToken(url: string): Promise<string> { function getJwtToken(url: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const httpClient = new DefaultHttpClient(NullLogger.instance);
httpClient.get(url).then((response) => {
xhr.open("GET", url, true); if (response.statusCode >= 200 && response.statusCode < 300) {
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); resolve(response.content as string);
xhr.send();
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response || xhr.responseText);
} else { } else {
reject(new Error(xhr.statusText)); reject(new Error(response.statusText));
} }
}; });
xhr.onerror = () => {
reject(new Error(xhr.statusText));
};
}); });
} }
}); });

View File

@ -6,28 +6,45 @@ import { ECHOENDPOINT_URL } from "./Common";
// On slower CI machines, these tests sometimes take longer than 5s // On slower CI machines, these tests sometimes take longer than 5s
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000;
if (typeof WebSocket !== "undefined") { describe("WebSockets", () => {
describe("WebSockets", () => { it("can be used to connect to SignalR", (done) => {
it("can be used to connect to SignalR", (done) => { const message = "message";
const message = "message";
const webSocket = new WebSocket(ECHOENDPOINT_URL.replace(/^http/, "ws"));
webSocket.onopen = () => {
webSocket.send(message);
};
webSocket.onmessage = (event) => {
expect(event.data).toEqual(message);
webSocket.close();
};
webSocket.onclose = (event) => {
expect(event.code).toEqual(1000);
expect(event.wasClean).toBe(true, "WebSocket did not close cleanly");
let webSocket: WebSocket;
if (typeof window !== "undefined") {
if (typeof WebSocket !== "undefined") {
webSocket = new WebSocket(ECHOENDPOINT_URL.replace(/^http/, "ws"));
} else {
// Running in a browser that doesn't support WebSockets
done(); done();
}; return;
}); }
} else {
const websocketModule = require("websocket");
const hasWebsocket = websocketModule && websocketModule.w3cwebsocket;
if (hasWebsocket) {
webSocket = new websocketModule.w3cwebsocket(ECHOENDPOINT_URL.replace(/^http/, "ws"));
} else {
// No WebSockets implementations in current environment, skip test
done();
return;
}
}
webSocket.onopen = () => {
webSocket.send(message);
};
webSocket.onmessage = (event) => {
expect(event.data).toEqual(message);
webSocket.close();
};
webSocket.onclose = (event) => {
expect(event.code).toEqual(1000);
expect(event.wasClean).toBe(true);
done();
};
}); });
} });

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
const path = require("path"); const path = require("path");
const webpack = require("../common/node_modules/webpack");
module.exports = { module.exports = {
entry: path.resolve(__dirname, "ts", "index.ts"), entry: path.resolve(__dirname, "ts", "index.ts"),

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@aspnet/signalr-protocol-msgpack", "name": "@aspnet/signalr-protocol-msgpack",
"version": "1.1.0-preview1-t000", "version": "3.0.0-alpha1-t000",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -26,13 +26,13 @@ export class MessagePackHubProtocol implements IHubProtocol {
/** Creates an array of HubMessage objects from the specified serialized representation. /** Creates an array of HubMessage objects from the specified serialized representation.
* *
* @param {ArrayBuffer} input An ArrayBuffer containing the serialized representation. * @param {ArrayBuffer | Buffer} input An ArrayBuffer containing the serialized representation.
* @param {ILogger} logger A logger that will be used to log messages that occur during parsing. * @param {ILogger} logger A logger that will be used to log messages that occur during parsing.
*/ */
public parseMessages(input: ArrayBuffer, logger: ILogger): HubMessage[] { public parseMessages(input: ArrayBuffer | Buffer, logger: ILogger): HubMessage[] {
// The interface does allow "string" to be passed in, but this implementation does not. So let's throw a useful error. // The interface does allow "string" to be passed in, but this implementation does not. So let's throw a useful error.
if (!(input instanceof ArrayBuffer)) { if (!(input instanceof ArrayBuffer) && !(input instanceof Buffer)) {
throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer."); throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer or Buffer.");
} }
if (logger === null) { if (logger === null) {

View File

@ -1,14 +1,124 @@
{ {
"name": "@aspnet/signalr", "name": "@aspnet/signalr",
"version": "1.1.0-preview1-t000", "version": "3.0.0-alpha1-t000",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@types/events": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
"dev": true
},
"@types/eventsource": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.0.2.tgz",
"integrity": "sha512-CprOekOB/lzAiGDF1MPWHX053RVTCYyYU3M8HOQXpdD0QfXijM//Na/hZxHaQv4ydsiB1uOBQ3p8S5nXpP4nNQ==",
"dev": true
},
"@types/node": {
"version": "10.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz",
"integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==",
"dev": true
},
"@types/websocket": {
"version": "0.0.40",
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-0.0.40.tgz",
"integrity": "sha512-ldteZwWIgl9cOy7FyvYn+39Ah4+PfpVE72eYKw75iy2L0zTbhbcwvzeJ5IOu6DQP93bjfXq0NGHY6FYtmYoqFQ==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/node": "*"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"es6-promise": { "es6-promise": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.2.tgz", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.2.tgz",
"integrity": "sha512-LSas5vsuA6Q4nEdf9wokY5/AJYXry98i0IzXsv49rYsgDGDNDPbqAYR1Pe23iFxygfbGZNR/5VrHXBCh2BhvUQ==", "integrity": "sha512-LSas5vsuA6Q4nEdf9wokY5/AJYXry98i0IzXsv49rYsgDGDNDPbqAYR1Pe23iFxygfbGZNR/5VrHXBCh2BhvUQ==",
"dev": true "dev": true
},
"eventsource": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
"integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
"requires": {
"original": "^1.0.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
"integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw=="
},
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
"integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
"requires": {
"url-parse": "^1.4.3"
}
},
"querystringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz",
"integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw=="
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"url-parse": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
"integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
"requires": {
"querystringify": "^2.0.0",
"requires-port": "^1.0.0"
}
},
"websocket": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz",
"integrity": "sha512-fjcrYDPIQxpTnqFQ9JjxUQcdvR89MFAOjPBlF+vjOt49w/XW4fJknUoMz/mDIn2eK1AdslVojcaOxOqyZZV8rw==",
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
"typedarray-to-buffer": "^3.1.2",
"yaeti": "^0.0.6"
}
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
} }
} }
} }

View File

@ -36,6 +36,12 @@
"src/**/*" "src/**/*"
], ],
"devDependencies": { "devDependencies": {
"es6-promise": "^4.2.2" "es6-promise": "^4.2.2",
"@types/websocket": "^0.0.40",
"@types/eventsource": "^1.0.2"
},
"dependencies": {
"websocket": "^1.0.26",
"eventsource": "^1.0.7"
} }
} }

View File

@ -0,0 +1,48 @@
// 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.
import { AbortError } from "./Errors";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger } from "./ILogger";
import { XhrHttpClient } from "./XhrHttpClient";
let nodeHttpClientModule: any;
if (typeof XMLHttpRequest === "undefined") {
// tslint:disable-next-line:no-var-requires
nodeHttpClientModule = require("./NodeHttpClient");
}
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */
export class DefaultHttpClient extends HttpClient {
private readonly httpClient: HttpClient;
/** Creates a new instance of the {@link @aspnet/signalr.DefaultHttpClient}, using the provided {@link @aspnet/signalr.ILogger} to log messages. */
public constructor(logger: ILogger) {
super();
if (typeof XMLHttpRequest !== "undefined") {
this.httpClient = new XhrHttpClient(logger);
} else if (typeof nodeHttpClientModule !== "undefined") {
this.httpClient = new nodeHttpClientModule.NodeHttpClient(logger);
} else {
throw new Error("No HttpClient could be created.");
}
}
/** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> {
// Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) {
return Promise.reject(new AbortError());
}
if (!request.method) {
return Promise.reject(new Error("No method defined."));
}
if (!request.url) {
return Promise.reject(new Error("No url defined."));
}
return this.httpClient.send(request);
}
}

View File

@ -27,7 +27,7 @@ export class HandshakeProtocol {
let messageData: string; let messageData: string;
let remainingData: any; let remainingData: any;
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer || (typeof Buffer !== "undefined" && data instanceof Buffer)) {
// Format is binary but still need to read JSON text from handshake response // Format is binary but still need to read JSON text from handshake response
const binaryData = new Uint8Array(data); const binaryData = new Uint8Array(data);
const separatorIndex = binaryData.indexOf(TextMessageFormat.RecordSeparatorCode); const separatorIndex = binaryData.indexOf(TextMessageFormat.RecordSeparatorCode);

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortSignal } from "./AbortController"; import { AbortSignal } from "./AbortController";
import { AbortError, HttpError, TimeoutError } from "./Errors";
import { ILogger, LogLevel } from "./ILogger";
/** Represents an HTTP request. */ /** Represents an HTTP request. */
export interface HttpRequest { export interface HttpRequest {
@ -144,89 +142,3 @@ export abstract class HttpClient {
*/ */
public abstract send(request: HttpRequest): Promise<HttpResponse>; public abstract send(request: HttpRequest): Promise<HttpResponse>;
} }
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */
export class DefaultHttpClient extends HttpClient {
private readonly logger: ILogger;
/** Creates a new instance of the {@link @aspnet/signalr.DefaultHttpClient}, using the provided {@link @aspnet/signalr.ILogger} to log messages. */
public constructor(logger: ILogger) {
super();
this.logger = logger;
}
/** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> {
return new Promise<HttpResponse>((resolve, reject) => {
// Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) {
reject(new AbortError());
return;
}
const xhr = new XMLHttpRequest();
if (!request.method) {
reject(new Error("No method defined."));
return;
}
if (!request.url) {
reject(new Error("No url defined."));
return;
}
xhr.open(request.method, request.url, true);
xhr.withCredentials = true;
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Explicitly setting the Content-Type header for React Native on Android platform.
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
const headers = request.headers;
if (headers) {
Object.keys(headers)
.forEach((header) => {
xhr.setRequestHeader(header, headers[header]);
});
}
if (request.responseType) {
xhr.responseType = request.responseType;
}
if (request.abortSignal) {
request.abortSignal.onabort = () => {
xhr.abort();
reject(new AbortError());
};
}
if (request.timeout) {
xhr.timeout = request.timeout;
}
xhr.onload = () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve(new HttpResponse(xhr.status, xhr.statusText, xhr.response || xhr.responseText));
} else {
reject(new HttpError(xhr.statusText, xhr.status));
}
};
xhr.onerror = () => {
this.logger.log(LogLevel.Warning, `Error from HTTP request. ${xhr.status}: ${xhr.statusText}`);
reject(new HttpError(xhr.statusText, xhr.status));
};
xhr.ontimeout = () => {
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
reject(new TimeoutError());
};
xhr.send(request.content || "");
});
}
}

View File

@ -1,7 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { DefaultHttpClient, HttpClient } from "./HttpClient"; import { DefaultHttpClient } from "./DefaultHttpClient";
import { HttpClient } from "./HttpClient";
import { IConnection } from "./IConnection"; import { IConnection } from "./IConnection";
import { IHttpConnectionOptions } from "./IHttpConnectionOptions"; import { IHttpConnectionOptions } from "./IHttpConnectionOptions";
import { ILogger, LogLevel } from "./ILogger"; import { ILogger, LogLevel } from "./ILogger";
@ -34,6 +35,15 @@ export interface IAvailableTransport {
const MAX_REDIRECTS = 100; const MAX_REDIRECTS = 100;
let WebSocketModule: any = null;
let EventSourceModule: any = null;
if (typeof window === "undefined" && typeof require !== "undefined") {
// tslint:disable-next-line:no-var-requires
WebSocketModule = require("websocket");
// tslint:disable-next-line:no-var-requires
EventSourceModule = require("eventsource");
}
/** @private */ /** @private */
export class HttpConnection implements IConnection { export class HttpConnection implements IConnection {
private connectionState: ConnectionState; private connectionState: ConnectionState;
@ -59,11 +69,22 @@ export class HttpConnection implements IConnection {
options = options || {}; options = options || {};
options.logMessageContent = options.logMessageContent || false; options.logMessageContent = options.logMessageContent || false;
if (typeof WebSocket !== "undefined" && !options.WebSocket) { const isNode = typeof window === "undefined";
if (!isNode && typeof WebSocket !== "undefined" && !options.WebSocket) {
options.WebSocket = WebSocket; options.WebSocket = WebSocket;
} else if (isNode && !options.WebSocket) {
const websocket = WebSocketModule && WebSocketModule.w3cwebsocket;
if (websocket) {
options.WebSocket = WebSocketModule.w3cwebsocket;
}
} }
if (typeof EventSource !== "undefined" && !options.EventSource) {
if (!isNode && typeof EventSource !== "undefined" && !options.EventSource) {
options.EventSource = EventSource; options.EventSource = EventSource;
} else if (isNode && !options.EventSource) {
if (typeof EventSourceModule !== "undefined") {
options.EventSource = EventSourceModule;
}
} }
this.httpClient = options.httpClient || new DefaultHttpClient(this.logger); this.httpClient = options.httpClient || new DefaultHttpClient(this.logger);
@ -103,6 +124,10 @@ export class HttpConnection implements IConnection {
public async stop(error?: Error): Promise<void> { public async stop(error?: Error): Promise<void> {
this.connectionState = ConnectionState.Disconnected; this.connectionState = ConnectionState.Disconnected;
// Set error as soon as possible otherwise there is a race between
// the transport closing and providing an error and the error from a close message
// We would prefer the close message error.
this.stopError = error;
try { try {
await this.startPromise; await this.startPromise;
@ -112,7 +137,6 @@ export class HttpConnection implements IConnection {
// The transport's onclose will trigger stopConnection which will run our onclose event. // The transport's onclose will trigger stopConnection which will run our onclose event.
if (this.transport) { if (this.transport) {
this.stopError = error;
await this.transport.stop(); await this.transport.stop();
this.transport = undefined; this.transport = undefined;
} }

View File

@ -52,8 +52,9 @@ export class LongPollingTransport implements ITransport {
this.logger.log(LogLevel.Trace, "(LongPolling transport) Connecting"); this.logger.log(LogLevel.Trace, "(LongPolling transport) Connecting");
if (transferFormat === TransferFormat.Binary && (typeof new XMLHttpRequest().responseType !== "string")) { // Allow binary format on Node and Browsers that support binary content (indicated by the presence of responseType property)
// This will work if we fix: https://github.com/aspnet/SignalR/issues/742 if (transferFormat === TransferFormat.Binary &&
(typeof XMLHttpRequest !== "undefined" && typeof new XMLHttpRequest().responseType !== "string")) {
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported."); throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
} }

View File

@ -0,0 +1,91 @@
// 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.
import * as http from "http";
import { URL } from "url";
import { AbortError, HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
export class NodeHttpClient extends HttpClient {
private readonly logger: ILogger;
public constructor(logger: ILogger) {
super();
this.logger = logger;
}
public send(request: HttpRequest): Promise<HttpResponse> {
return new Promise<HttpResponse>((resolve, reject) => {
const url = new URL(request.url!);
const options: http.RequestOptions = {
headers: {
// Tell auth middleware to 401 instead of redirecting
"X-Requested-With": "XMLHttpRequest",
...request.headers,
},
hostname: url.hostname,
method: request.method,
// /abc/xyz + ?id=12ssa_30
path: url.pathname + url.search,
port: url.port,
};
const req = http.request(options, (res: http.IncomingMessage) => {
const data: Buffer[] = [];
let dataLength = 0;
res.on("data", (chunk: any) => {
data.push(chunk);
// Buffer.concat will be slightly faster if we keep track of the length
dataLength += chunk.length;
});
res.on("end", () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
let resp: string | ArrayBuffer;
if (request.responseType === "arraybuffer") {
resp = Buffer.concat(data, dataLength);
resolve(new HttpResponse(res.statusCode, res.statusMessage || "", resp));
} else {
resp = Buffer.concat(data, dataLength).toString();
resolve(new HttpResponse(res.statusCode, res.statusMessage || "", resp));
}
} else {
reject(new HttpError(res.statusMessage || "", res.statusCode || 0));
}
});
});
if (request.abortSignal) {
request.abortSignal.onabort = () => {
req.abort();
reject(new AbortError());
};
}
if (request.timeout) {
req.setTimeout(request.timeout, () => {
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
reject(new TimeoutError());
});
}
req.on("error", (e) => {
this.logger.log(LogLevel.Warning, `Error from HTTP request. ${e}`);
reject(e);
});
if (request.content instanceof ArrayBuffer) {
req.write(Buffer.from(request.content));
} else {
req.write(request.content || "");
}
req.end();
});
}
}

View File

@ -68,9 +68,11 @@ export async function sendMessage(logger: ILogger, transportName: string, httpCl
logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`); logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`);
const responseType = content instanceof ArrayBuffer ? "arraybuffer" : "text";
const response = await httpClient.post(url, { const response = await httpClient.post(url, {
content, content,
headers, headers,
responseType,
}); });
logger.log(LogLevel.Trace, `(${transportName} transport) request complete. Response status: ${response.statusCode}.`); logger.log(LogLevel.Trace, `(${transportName} transport) request complete. Response status: ${response.statusCode}.`);

View File

@ -57,7 +57,11 @@ export class WebSocketTransport implements ITransport {
}; };
webSocket.onerror = (event: Event) => { webSocket.onerror = (event: Event) => {
const error = (event instanceof ErrorEvent) ? event.error : null; let error: any = null;
// ErrorEvent is a browser only type we need to check if the type exists before using it
if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
error = event.error;
}
reject(error); reject(error);
}; };

View File

@ -0,0 +1,87 @@
// 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.
import { AbortError, HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
export class XhrHttpClient extends HttpClient {
private readonly logger: ILogger;
public constructor(logger: ILogger) {
super();
this.logger = logger;
}
/** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> {
// Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) {
return Promise.reject(new AbortError());
}
if (!request.method) {
return Promise.reject(new Error("No method defined."));
}
if (!request.url) {
return Promise.reject(new Error("No url defined."));
}
return new Promise<HttpResponse>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(request.method!, request.url!, true);
xhr.withCredentials = true;
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Explicitly setting the Content-Type header for React Native on Android platform.
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
const headers = request.headers;
if (headers) {
Object.keys(headers)
.forEach((header) => {
xhr.setRequestHeader(header, headers[header]);
});
}
if (request.responseType) {
xhr.responseType = request.responseType;
}
if (request.abortSignal) {
request.abortSignal.onabort = () => {
xhr.abort();
reject(new AbortError());
};
}
if (request.timeout) {
xhr.timeout = request.timeout;
}
xhr.onload = () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve(new HttpResponse(xhr.status, xhr.statusText, xhr.response || xhr.responseText));
} else {
reject(new HttpError(xhr.statusText, xhr.status));
}
};
xhr.onerror = () => {
this.logger.log(LogLevel.Warning, `Error from HTTP request. ${xhr.status}: ${xhr.statusText}`);
reject(new HttpError(xhr.statusText, xhr.status));
};
xhr.ontimeout = () => {
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
reject(new TimeoutError());
};
xhr.send(request.content || "");
});
}
}

View File

@ -8,7 +8,8 @@ export const VERSION: string = "0.0.0-DEV_BUILD";
// Everything that users need to access must be exported here. Including interfaces. // Everything that users need to access must be exported here. Including interfaces.
export { AbortSignal } from "./AbortController"; export { AbortSignal } from "./AbortController";
export { AbortError, HttpError, TimeoutError } from "./Errors"; export { AbortError, HttpError, TimeoutError } from "./Errors";
export { DefaultHttpClient, HttpClient, HttpRequest, HttpResponse } from "./HttpClient"; export { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
export { DefaultHttpClient } from "./DefaultHttpClient";
export { IHttpConnectionOptions } from "./IHttpConnectionOptions"; export { IHttpConnectionOptions } from "./IHttpConnectionOptions";
export { HubConnection, HubConnectionState } from "./HubConnection"; export { HubConnection, HubConnectionState } from "./HubConnection";
export { HubConnectionBuilder } from "./HubConnectionBuilder"; export { HubConnectionBuilder } from "./HubConnectionBuilder";

View File

@ -12,7 +12,7 @@ import { EventSourceConstructor, WebSocketConstructor } from "../src/Polyfills";
import { eachEndpointUrl, eachTransport, VerifyLogger } from "./Common"; import { eachEndpointUrl, eachTransport, VerifyLogger } from "./Common";
import { TestHttpClient } from "./TestHttpClient"; import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource, registerUnhandledRejectionHandler } from "./Utils"; import { PromiseSource, registerUnhandledRejectionHandler, SyncPoint } from "./Utils";
const commonOptions: IHttpConnectionOptions = { const commonOptions: IHttpConnectionOptions = {
logger: NullLogger.instance, logger: NullLogger.instance,
@ -320,8 +320,30 @@ describe("HttpConnection", () => {
for (const [val, name] of [[null, "null"], [undefined, "undefined"], [0, "0"]]) { for (const [val, name] of [[null, "null"], [undefined, "undefined"], [0, "0"]]) {
it(`can be started when transport mask is ${name}`, async () => { it(`can be started when transport mask is ${name}`, async () => {
let websocketOpen: (() => any) | null = null;
const sync: SyncPoint = new SyncPoint();
const websocket = class WebSocket {
constructor() {
this._onopen = null;
}
// tslint:disable-next-line:variable-name
private _onopen: ((this: WebSocket, ev: Event) => any) | null;
public get onopen(): ((this: WebSocket, ev: Event) => any) | null {
return this._onopen;
}
public set onopen(onopen: ((this: WebSocket, ev: Event) => any) | null) {
this._onopen = onopen;
websocketOpen = () => this._onopen!({} as Event);
sync.continue();
}
public close(): void {
}
};
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
WebSocket: websocket as any,
...commonOptions, ...commonOptions,
httpClient: new TestHttpClient() httpClient: new TestHttpClient()
.on("POST", () => defaultNegotiateResponse) .on("POST", () => defaultNegotiateResponse)
@ -333,7 +355,10 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", options);
await connection.start(TransferFormat.Text); const startPromise = connection.start(TransferFormat.Text);
await sync.waitToContinue();
websocketOpen!();
await startPromise;
await connection.stop(); await connection.stop();
}); });
@ -359,10 +384,18 @@ describe("HttpConnection", () => {
}); });
it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => { it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => {
const websocket = class WebSocket {
constructor() {
throw new Error("WebSocket constructor called.");
}
};
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
WebSocket: websocket as any,
...commonOptions, ...commonOptions,
httpClient: new TestHttpClient(), httpClient: new TestHttpClient()
.on("POST", () => { throw new Error("Should not be called"); })
.on("GET", () => { throw new Error("Should not be called"); }),
logger, logger,
skipNegotiation: true, skipNegotiation: true,
transport: HttpTransportType.WebSockets, transport: HttpTransportType.WebSockets,
@ -371,9 +404,9 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options); const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text)) await expect(connection.start(TransferFormat.Text))
.rejects .rejects
.toThrow("'WebSocket' is not supported in your environment."); .toThrow("WebSocket constructor called.");
}, },
"Failed to start the connection: Error: 'WebSocket' is not supported in your environment."); "Failed to start the connection: Error: WebSocket constructor called.");
}); });
it("does not start non WebSockets transport if requested explicitly and skipNegotiation is true", async () => { it("does not start non WebSockets transport if requested explicitly and skipNegotiation is true", async () => {
@ -603,120 +636,12 @@ describe("HttpConnection", () => {
}); });
}); });
it("does not select ServerSentEvents transport when not available in environment", async () => {
await VerifyLogger.run(async (logger) => {
const serverSentEventsTransport = { transport: "ServerSentEvents", transferFormats: ["Text"] };
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", () => ({ connectionId: "42", availableTransports: [serverSentEventsTransport] })),
logger,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
},
"Failed to start the connection: Error: Unable to initialize any of the available transports.");
});
it("does not select WebSockets transport when not available in environment", async () => {
await VerifyLogger.run(async (logger) => {
const webSocketsTransport = { transport: "WebSockets", transferFormats: ["Text"] };
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", () => ({ connectionId: "42", availableTransports: [webSocketsTransport] })),
logger,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
},
"Failed to start the connection: Error: Unable to initialize any of the available transports.");
});
describe(".constructor", () => { describe(".constructor", () => {
it("throws if no Url is provided", async () => { it("throws if no Url is provided", async () => {
// Force TypeScript to let us call the constructor incorrectly :) // Force TypeScript to let us call the constructor incorrectly :)
expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required."); expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required.");
}); });
it("uses global WebSocket if defined", async () => {
await VerifyLogger.run(async (logger) => {
// tslint:disable-next-line:no-string-literal
global["WebSocket"] = class WebSocket {
constructor() {
throw new Error("WebSocket constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
logger,
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start())
.rejects
.toThrow("WebSocket constructor called.");
// tslint:disable-next-line:no-string-literal
delete global["WebSocket"];
},
"Failed to start the connection: Error: WebSocket constructor called.");
});
it("uses global EventSource if defined", async () => {
await VerifyLogger.run(async (logger) => {
let eventSourceConstructorCalled: boolean = false;
// tslint:disable-next-line:no-string-literal
global["EventSource"] = class EventSource {
constructor() {
eventSourceConstructorCalled = true;
throw new Error("EventSource constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient().on("POST", () => {
return {
availableTransports: [
{ transport: "ServerSentEvents", transferFormats: ["Text"] },
],
connectionId: defaultConnectionId,
};
}),
logger,
transport: HttpTransportType.ServerSentEvents,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
expect(eventSourceConstructorCalled).toEqual(true);
// tslint:disable-next-line:no-string-literal
delete global["EventSource"];
},
"Failed to start the transport 'ServerSentEvents': Error: EventSource constructor called.",
"Failed to start the connection: Error: Unable to initialize any of the available transports.");
});
it("uses EventSource constructor from options if provided", async () => { it("uses EventSource constructor from options if provided", async () => {
await VerifyLogger.run(async (logger) => { await VerifyLogger.run(async (logger) => {
let eventSourceConstructorCalled: boolean = false; let eventSourceConstructorCalled: boolean = false;

View File

@ -2,4 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
const baseConfig = require("../webpack.config.base"); const baseConfig = require("../webpack.config.base");
module.exports = baseConfig(__dirname, "signalr"); module.exports = baseConfig(__dirname, "signalr", {
// These are only used in Node environments
// so we tell webpack not to pull them in for the browser
externals: [
"websocket",
"eventsource",
"http",
"url",
]
});

View File

@ -6,7 +6,8 @@
"noUnusedParameters": false, "noUnusedParameters": false,
"typeRoots": [ "typeRoots": [
"./common/node_modules/@types" "./common/node_modules/@types"
] ],
"allowJs": true
}, },
"include": [ "include": [
"./*/tests/**/*" "./*/tests/**/*"

View File

@ -72,6 +72,9 @@ module.exports = function (modulePath, browserBaseName, options) {
}), }),
// ES6 Promise uses this module in certain circumstances but we don't need it. // ES6 Promise uses this module in certain circumstances but we don't need it.
new webpack.IgnorePlugin(/vertx/), new webpack.IgnorePlugin(/vertx/),
new webpack.IgnorePlugin(/NodeHttpClient/),
new webpack.IgnorePlugin(/eventsource/),
new webpack.IgnorePlugin(/websocket/),
], ],
externals: options.externals, externals: options.externals,
}; };

View File

@ -218,8 +218,18 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// Cancel the previous request // Cancel the previous request
connection.Cancellation?.Cancel(); connection.Cancellation?.Cancel();
// Always wait for the previous request to drain try
await connection.PreviousPollTask; {
// Wait for the previous request to drain
await connection.PreviousPollTask;
}
catch (OperationCanceledException)
{
// Previous poll canceled due to connection closing, close this poll too
context.Response.ContentType = "text/plain";
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
connection.PreviousPollTask = currentRequestTcs.Task; connection.PreviousPollTask = currentRequestTcs.Task;
} }
@ -289,6 +299,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// If the status code is a 204 it means the connection is done // If the status code is a 204 it means the connection is done
if (context.Response.StatusCode == StatusCodes.Status204NoContent) if (context.Response.StatusCode == StatusCodes.Status204NoContent)
{ {
// Cancel current request to release any waiting poll and let dispose aquire the lock
currentRequestTcs.TrySetCanceled();
// We should be able to safely dispose because there's no more data being written // We should be able to safely dispose because there's no more data being written
// We don't need to wait for close here since we've already waited for both sides // We don't need to wait for close here since we've already waited for both sides
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false); await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);
@ -299,6 +312,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
else if (resultTask.IsFaulted) else if (resultTask.IsFaulted)
{ {
// Cancel current request to release any waiting poll and let dispose aquire the lock
currentRequestTcs.TrySetCanceled();
// transport task was faulted, we should remove the connection // transport task was faulted, we should remove the connection
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false); await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);

View File

@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
return PublishAsync(_channels.Group(groupName), message); return PublishAsync(_channels.Group(groupName), message);
} }
public override async Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default) public override Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
{ {
if (groupName == null) if (groupName == null)
{ {
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
} }
var message = _protocol.WriteInvocation(methodName, args, excludedConnectionIds); var message = _protocol.WriteInvocation(methodName, args, excludedConnectionIds);
await PublishAsync(_channels.Group(groupName), message); return PublishAsync(_channels.Group(groupName), message);
} }
public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default) public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default)
@ -160,7 +160,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
return PublishAsync(_channels.User(userId), message); return PublishAsync(_channels.User(userId), message);
} }
public override async Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
{ {
if (connectionId == null) if (connectionId == null)
{ {
@ -176,14 +176,13 @@ namespace Microsoft.AspNetCore.SignalR.Redis
if (connection != null) if (connection != null)
{ {
// short circuit if connection is on this server // short circuit if connection is on this server
await AddGroupAsyncCore(connection, groupName); return AddGroupAsyncCore(connection, groupName);
return;
} }
await SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Add); return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Add);
} }
public override async Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
{ {
if (connectionId == null) if (connectionId == null)
{ {
@ -199,11 +198,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
if (connection != null) if (connection != null)
{ {
// short circuit if connection is on this server // short circuit if connection is on this server
await RemoveGroupAsyncCore(connection, groupName); return RemoveGroupAsyncCore(connection, groupName);
return;
} }
await SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Remove); return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Remove);
} }
public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default) public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
@ -271,7 +269,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
await _bus.PublishAsync(channel, payload); await _bus.PublishAsync(channel, payload);
} }
private async Task AddGroupAsyncCore(HubConnectionContext connection, string groupName) private Task AddGroupAsyncCore(HubConnectionContext connection, string groupName)
{ {
var feature = connection.Features.Get<IRedisFeature>(); var feature = connection.Features.Get<IRedisFeature>();
var groupNames = feature.Groups; var groupNames = feature.Groups;
@ -281,12 +279,12 @@ namespace Microsoft.AspNetCore.SignalR.Redis
// Connection already in group // Connection already in group
if (!groupNames.Add(groupName)) if (!groupNames.Add(groupName))
{ {
return; return Task.CompletedTask;
} }
} }
var groupChannel = _channels.Group(groupName); var groupChannel = _channels.Group(groupName);
await _groups.AddSubscriptionAsync(groupChannel, connection, SubscribeToGroupAsync); return _groups.AddSubscriptionAsync(groupChannel, connection, SubscribeToGroupAsync);
} }
/// <summary> /// <summary>
@ -297,10 +295,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{ {
var groupChannel = _channels.Group(groupName); var groupChannel = _channels.Group(groupName);
await _groups.RemoveSubscriptionAsync(groupChannel, connection, async channelName => await _groups.RemoveSubscriptionAsync(groupChannel, connection, channelName =>
{ {
RedisLog.Unsubscribe(_logger, channelName); RedisLog.Unsubscribe(_logger, channelName);
await _bus.UnsubscribeAsync(channelName); return _bus.UnsubscribeAsync(channelName);
}); });
var feature = connection.Features.Get<IRedisFeature>(); var feature = connection.Features.Get<IRedisFeature>();
@ -329,10 +327,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{ {
var userChannel = _channels.User(connection.UserIdentifier); var userChannel = _channels.User(connection.UserIdentifier);
return _users.RemoveSubscriptionAsync(userChannel, connection, async channelName => return _users.RemoveSubscriptionAsync(userChannel, connection, channelName =>
{ {
RedisLog.Unsubscribe(_logger, channelName); RedisLog.Unsubscribe(_logger, channelName);
await _bus.UnsubscribeAsync(channelName); return _bus.UnsubscribeAsync(channelName);
}); });
} }
@ -343,10 +341,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
_ackHandler.Dispose(); _ackHandler.Dispose();
} }
private void SubscribeToAll() private Task SubscribeToAll()
{ {
RedisLog.Subscribing(_logger, _channels.All); RedisLog.Subscribing(_logger, _channels.All);
_bus.Subscribe(_channels.All, async (c, data) => return _bus.SubscribeAsync(_channels.All, async (c, data) =>
{ {
try try
{ {
@ -373,9 +371,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis
}); });
} }
private void SubscribeToGroupManagementChannel() private Task SubscribeToGroupManagementChannel()
{ {
_bus.Subscribe(_channels.GroupManagement, async (c, data) => return _bus.SubscribeAsync(_channels.GroupManagement, async (c, data) =>
{ {
try try
{ {
@ -408,10 +406,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
}); });
} }
private void SubscribeToAckChannel() private Task SubscribeToAckChannel()
{ {
// Create server specific channel in order to send an ack to a single server // Create server specific channel in order to send an ack to a single server
_bus.Subscribe(_channels.Ack(_serverName), (c, data) => return _bus.SubscribeAsync(_channels.Ack(_serverName), (c, data) =>
{ {
var ackId = _protocol.ReadAck((byte[])data); var ackId = _protocol.ReadAck((byte[])data);
@ -435,9 +433,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{ {
var userChannel = _channels.User(connection.UserIdentifier); var userChannel = _channels.User(connection.UserIdentifier);
return _users.AddSubscriptionAsync(userChannel, connection, async (channelName, subscriptions) => return _users.AddSubscriptionAsync(userChannel, connection, (channelName, subscriptions) =>
{ {
await _bus.SubscribeAsync(channelName, async (c, data) => RedisLog.Subscribing(_logger, channelName);
return _bus.SubscribeAsync(channelName, async (c, data) =>
{ {
try try
{ {
@ -534,9 +533,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis
RedisLog.NotConnected(_logger); RedisLog.NotConnected(_logger);
} }
SubscribeToAll(); await SubscribeToAll();
SubscribeToGroupManagementChannel(); await SubscribeToGroupManagementChannel();
SubscribeToAckChannel(); await SubscribeToAckChannel();
} }
} }
finally finally

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
private static readonly string _dockerContainerName = "redisTestContainer"; private static readonly string _dockerContainerName = "redisTestContainer";
private static readonly string _dockerMonitorContainerName = _dockerContainerName + "Monitor";
private static readonly Lazy<Docker> _instance = new Lazy<Docker>(Create); private static readonly Lazy<Docker> _instance = new Lazy<Docker>(Create);
public static Docker Default => _instance.Value; public static Docker Default => _instance.Value;
@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
var docker = new Docker(location); var docker = new Docker(location);
docker.RunCommand("info --format '{{.OSType}}'", out var output); docker.RunCommand("info --format '{{.OSType}}'", "docker info", out var output);
if (!string.Equals(output.Trim('\'', '"', '\r', '\n', ' '), "linux")) if (!string.Equals(output.Trim('\'', '"', '\r', '\n', ' '), "linux"))
{ {
@ -74,40 +75,49 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
logger.LogInformation("Starting docker container"); logger.LogInformation("Starting docker container");
// stop container if there is one, could be from a previous test run, ignore failures // stop container if there is one, could be from a previous test run, ignore failures
RunProcess(_path, $"stop {_dockerContainerName}", logger, TimeSpan.FromSeconds(5), out var output); RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var output);
// create and run docker container, remove automatically when stopped, map 6379 from the container to 6379 localhost // create and run docker container, remove automatically when stopped, map 6379 from the container to 6379 localhost
// use static name 'redisTestContainer' so if the container doesn't get removed we don't keep adding more // use static name 'redisTestContainer' so if the container doesn't get removed we don't keep adding more
// use redis base docker image // use redis base docker image
// 20 second timeout to allow redis image to be downloaded, should be a rare occurance, only happening when a new version is released // 20 second timeout to allow redis image to be downloaded, should be a rare occurance, only happening when a new version is released
RunProcessAndThrowIfFailed(_path, $"run --rm -p 6379:6379 --name {_dockerContainerName} -d redis", logger, TimeSpan.FromSeconds(20)); RunProcessAndThrowIfFailed(_path, $"run --rm -p 6379:6379 --name {_dockerContainerName} -d redis", "redis", logger, TimeSpan.FromSeconds(20));
// inspect the redis docker image and extract the IPAddress. Necessary when running tests from inside a docker container, spinning up a new docker container for redis // inspect the redis docker image and extract the IPAddress. Necessary when running tests from inside a docker container, spinning up a new docker container for redis
// outside the current container requires linking the networks (difficult to automate) or using the IP:Port combo // outside the current container requires linking the networks (difficult to automate) or using the IP:Port combo
RunProcess(_path, "inspect --format=\"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + _dockerContainerName, logger, TimeSpan.FromSeconds(5), out output); RunProcessAndWait(_path, "inspect --format=\"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + _dockerContainerName, "docker ipaddress", logger, TimeSpan.FromSeconds(5), out output);
output = output.Trim().Replace(Environment.NewLine, ""); output = output.Trim().Replace(Environment.NewLine, "");
// variable used by Startup.cs // variable used by Startup.cs
Environment.SetEnvironmentVariable("REDIS_CONNECTION", $"{output}:6379"); Environment.SetEnvironmentVariable("REDIS_CONNECTION", $"{output}:6379");
var (monitorProcess, monitorOutput) = RunProcess(_path, $"run -i --name {_dockerMonitorContainerName} --link {_dockerContainerName}:redis --rm redis redis-cli -h redis -p 6379", "redis monitor", logger);
monitorProcess.StandardInput.WriteLine("MONITOR");
monitorProcess.StandardInput.Flush();
} }
public void Stop(ILogger logger) public void Stop(ILogger logger)
{ {
// Get logs from Redis container before stopping the container
RunProcessAndThrowIfFailed(_path, $"logs {_dockerContainerName}", "docker logs", logger, TimeSpan.FromSeconds(5));
logger.LogInformation("Stopping docker container"); logger.LogInformation("Stopping docker container");
RunProcessAndThrowIfFailed(_path, $"stop {_dockerContainerName}", logger, TimeSpan.FromSeconds(5)); RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
} }
public int RunCommand(string commandAndArguments, out string output) => public int RunCommand(string commandAndArguments, string prefix, out string output) =>
RunCommand(commandAndArguments, NullLogger.Instance, out output); RunCommand(commandAndArguments, prefix, NullLogger.Instance, out output);
public int RunCommand(string commandAndArguments, ILogger logger, out string output) public int RunCommand(string commandAndArguments, string prefix, ILogger logger, out string output)
{ {
return RunProcess(_path, commandAndArguments, logger, TimeSpan.FromSeconds(5), out output); return RunProcessAndWait(_path, commandAndArguments, prefix, logger, TimeSpan.FromSeconds(5), out output);
} }
private static void RunProcessAndThrowIfFailed(string fileName, string arguments, ILogger logger, TimeSpan timeout) private static void RunProcessAndThrowIfFailed(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout)
{ {
var exitCode = RunProcess(fileName, arguments, logger, timeout, out var output); var exitCode = RunProcessAndWait(fileName, arguments, prefix, logger, timeout, out var output);
if (exitCode != 0) if (exitCode != 0)
{ {
@ -115,39 +125,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
} }
} }
private static int RunProcess(string fileName, string arguments, ILogger logger, TimeSpan timeout, out string output) private static int RunProcessAndWait(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout, out string output)
{ {
var process = new Process var (process, lines) = RunProcess(fileName, arguments, prefix, logger);
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true
},
EnableRaisingEvents = true
};
var exitCode = 0;
var lines = new ConcurrentQueue<string>();
process.Exited += (_, __) => exitCode = process.ExitCode;
process.OutputDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogInformation, "stdout: {0}", a.Data);
lines.Enqueue(a.Data);
};
process.ErrorDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogError, "stderr: {0}", a.Data);
lines.Enqueue(a.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
if (!process.WaitForExit((int)timeout.TotalMilliseconds)) if (!process.WaitForExit((int)timeout.TotalMilliseconds))
{ {
@ -160,7 +140,45 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
output = string.Join(Environment.NewLine, lines); output = string.Join(Environment.NewLine, lines);
return exitCode; return process.ExitCode;
}
private static (Process, ConcurrentQueue<string>) RunProcess(string fileName, string arguments, string prefix, ILogger logger)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true
},
EnableRaisingEvents = true
};
var exitCode = 0;
var lines = new ConcurrentQueue<string>();
process.Exited += (_, __) => exitCode = process.ExitCode;
process.OutputDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogInformation, $"'{prefix}' stdout: {{0}}", a.Data);
lines.Enqueue(a.Data);
};
process.ErrorDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogError, $"'{prefix}' stderr: {{0}}", a.Data);
lines.Enqueue(a.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
return (process, lines);
} }
private static void LogIfNotNull(Action<string, object[]> logger, string message, string data) private static void LogIfNotNull(Action<string, object[]> logger, string message, string data)

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
if(Docker.Default != null) if(Docker.Default != null)
{ {
// Docker is present, but is it working? // Docker is present, but is it working?
if (Docker.Default.RunCommand("ps", out var output) != 0) if (Docker.Default.RunCommand("ps", "docker ps", out var output) != 0)
{ {
SkipReason = $"Failed to invoke test command 'docker ps'. Output: {output}"; SkipReason = $"Failed to invoke test command 'docker ps'. Output: {output}";
} }