From 4898c0d3dfe5516c7e4285f76a5dcea102d48600 Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Mon, 7 Aug 2017 14:41:35 -0700 Subject: [PATCH] Integrating MsgPack support in TS client --- .../Connection.spec.ts | 60 +++++++++++++++---- .../HubConnection.spec.ts | 23 +++---- .../HttpConnection.ts | 15 ++++- .../HubConnection.ts | 59 ++++++++++++++++-- .../IConnection.ts | 4 +- .../IHubProtocol.ts | 8 ++- .../JsonHubProtocol.ts | 9 +-- .../MessagePackHubProtocol.ts | 9 +-- .../Transports.ts | 45 +++++++++----- .../gulpfile.js | 10 +++- ...soft.AspNetCore.SignalR.Test.Server.csproj | 3 +- .../wwwroot/connectionTests.html | 3 +- .../wwwroot/js/common.js | 15 ++++- .../wwwroot/js/hubConnectionTests.js | 46 +++++++------- .../Protocol/MessagePackHubProtocol.cs | 2 +- 15 files changed, 230 insertions(+), 81 deletions(-) diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts index 7c73028fbb..32defd999c 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts @@ -2,7 +2,7 @@ import { IHttpClient } from "../Microsoft.AspNetCore.SignalR.Client.TS/HttpClien import { HttpConnection } from "../Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection" import { IHttpConnectionOptions } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHttpConnectionOptions" import { DataReceived, TransportClosed } from "../Microsoft.AspNetCore.SignalR.Client.TS/Common" -import { ITransport, TransportType } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports" +import { ITransport, TransportType, TransferMode } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports" import { eachTransport } from "./Common"; describe("Connection", () => { @@ -112,7 +112,7 @@ describe("Connection", () => { } } as IHttpConnectionOptions; - var connection = new HttpConnection("http://tempuri.org", options); + let connection = new HttpConnection("http://tempuri.org", options); try { await connection.start(); @@ -125,7 +125,7 @@ describe("Connection", () => { }); it("can stop a non-started connection", async (done) => { - var connection = new HttpConnection("http://tempuri.org"); + let connection = new HttpConnection("http://tempuri.org"); await connection.stop(); done(); }); @@ -133,16 +133,16 @@ describe("Connection", () => { it("preserves users connection string", async done => { let connectUrl: string; let fakeTransport: ITransport = { - connect(url: string): Promise { + connect(url: string): Promise { connectUrl = url; - return Promise.reject(""); + return Promise.reject(TransferMode.Text); }, send(data: any): Promise { return Promise.reject(""); }, stop(): void { }, onDataReceived: undefined, - onClosed: undefined + onClosed: undefined, } let options: IHttpConnectionOptions = { @@ -158,7 +158,7 @@ describe("Connection", () => { } as IHttpConnectionOptions; - var connection = new HttpConnection("http://tempuri.org?q=myData", options); + let connection = new HttpConnection("http://tempuri.org?q=myData", options); try { await connection.start(); @@ -173,7 +173,7 @@ describe("Connection", () => { }); eachTransport((requestedTransport: TransportType) => { - it(`Connection cannot be started if requested ${TransportType[requestedTransport]} transport not available on server`, async done => { + it(`cannot be started if requested ${TransportType[requestedTransport]} transport not available on server`, async done => { let options: IHttpConnectionOptions = { httpClient: { options(url: string): Promise { @@ -186,7 +186,7 @@ describe("Connection", () => { transport: requestedTransport } as IHttpConnectionOptions; - var connection = new HttpConnection("http://tempuri.org", options); + let connection = new HttpConnection("http://tempuri.org", options); try { await connection.start(); fail(); @@ -199,7 +199,7 @@ describe("Connection", () => { }); }); - it(`Connection cannot be started if no transport available on server and no transport requested`, async done => { + it("cannot be started if no transport available on server and no transport requested", async done => { let options: IHttpConnectionOptions = { httpClient: { options(url: string): Promise { @@ -211,7 +211,7 @@ describe("Connection", () => { } } as IHttpConnectionOptions; - var connection = new HttpConnection("http://tempuri.org", options); + let connection = new HttpConnection("http://tempuri.org", options); try { await connection.start(); fail(); @@ -222,4 +222,42 @@ describe("Connection", () => { done(); } }); + + [ + [TransferMode.Text, TransferMode.Text], + [TransferMode.Text, TransferMode.Binary], + [TransferMode.Binary, TransferMode.Text], + [TransferMode.Binary, TransferMode.Binary], + ].forEach(([requestedTransferMode, transportTransferMode]) => { + it(`connection returns ${transportTransferMode} transfer mode when ${requestedTransferMode} transfer mode is requested`, async () => { + let fakeTransport = { + // mode: TransferMode : TransferMode.Text + connect(url: string, requestedTransferMode: TransferMode): Promise { return Promise.resolve(transportTransferMode); }, + send(data: any): Promise { return Promise.resolve(); }, + stop(): void {}, + onDataReceived: null, + onClosed: null, + mode: transportTransferMode + } as ITransport; + + let options: IHttpConnectionOptions = { + httpClient: { + options(url: string): Promise { + return Promise.resolve("{ \"connectionId\": \"42\", \"availableTransports\": [] }"); + }, + get(url: string): Promise { + return Promise.resolve(""); + } + }, + transport: fakeTransport + } as IHttpConnectionOptions; + + let connection = new HttpConnection("https://tempuri.org", options); + connection.features.transferMode = requestedTransferMode; + await connection.start(); + let actualTransferMode = connection.features.transferMode; + + expect(actualTransferMode).toBe(transportTransferMode); + }); + }); }); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts index e83e6a2cf0..20cf28ffcf 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts @@ -1,7 +1,7 @@ import { IConnection } from "../Microsoft.AspNetCore.SignalR.Client.TS/IConnection" import { HubConnection } from "../Microsoft.AspNetCore.SignalR.Client.TS/HubConnection" import { DataReceived, ConnectionClosed } from "../Microsoft.AspNetCore.SignalR.Client.TS/Common" -import { TransportType, ITransport } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports" +import { TransportType, ITransport, TransferMode } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports" import { Observer } from "../Microsoft.AspNetCore.SignalR.Client.TS/Observable" import { TextMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/Formatters" @@ -26,7 +26,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.send("testMethod", "arg", 42) + let invokePromise = hubConnection.send("testMethod", "arg", 42) .catch((_) => { }); // Suppress exception and unhandled promise rejection warning. // Verify the message is sent @@ -52,7 +52,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.invoke("testMethod", "arg", 42) + let invokePromise = hubConnection.invoke("testMethod", "arg", 42) .catch((_) => { }); // Suppress exception and unhandled promise rejection warning. // Verify the message is sent @@ -76,7 +76,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.invoke("testMethod", "arg", 42); + let invokePromise = hubConnection.invoke("testMethod", "arg", 42); connection.receive({ type: 3, invocationId: connection.lastInvocationId, error: "foo" }); @@ -88,7 +88,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.invoke("testMethod", "arg", 42); + let invokePromise = hubConnection.invoke("testMethod", "arg", 42); connection.receive({ type: 3, invocationId: connection.lastInvocationId, result: "foo" }); @@ -99,7 +99,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.invoke("testMethod"); + let invokePromise = hubConnection.invoke("testMethod"); hubConnection.stop(); let ex = await captureException(async () => await invokePromise); @@ -110,7 +110,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.invoke("testMethod"); + let invokePromise = hubConnection.invoke("testMethod"); // Typically this would be called by the transport connection.onClosed(new Error("Connection lost")); @@ -137,7 +137,7 @@ describe("HubConnection", () => { let connection = new TestConnection(); let hubConnection = new HubConnection(connection); - var invokePromise = hubConnection.stream("testStream", "arg", 42); + let invokePromise = hubConnection.stream("testStream", "arg", 42); // Verify the message is sent expect(connection.sentData.length).toBe(1); @@ -168,7 +168,6 @@ describe("HubConnection", () => { let ex = await captureException(async () => await observer.completed); expect(ex.message).toEqual("Error: foo"); - }); it("completes the observer when a completion is received", async () => { @@ -250,12 +249,14 @@ describe("HubConnection", () => { }); class TestConnection implements IConnection { + readonly features: any = {}; + start(): Promise { return Promise.resolve(); }; send(data: any): Promise { - var invocation = TextMessageFormat.parse(data)[0]; + let invocation = TextMessageFormat.parse(data)[0]; this.lastInvocationId = JSON.parse(invocation).invocationId; if (this.sentData) { this.sentData.push(invocation); @@ -273,7 +274,7 @@ class TestConnection implements IConnection { }; receive(data: any): void { - var payload = JSON.stringify(data); + let payload = JSON.stringify(data); this.onDataReceived(TextMessageFormat.write(payload)); } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts index 8d1cd751f1..38854dab8a 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts @@ -1,10 +1,10 @@ import { DataReceived, ConnectionClosed } from "./Common" import { IConnection } from "./IConnection" -import { ITransport, TransportType, WebSocketTransport, ServerSentEventsTransport, LongPollingTransport } from "./Transports" +import { ITransport, TransferMode, TransportType, WebSocketTransport, ServerSentEventsTransport, LongPollingTransport } from "./Transports" import { IHttpClient, HttpClient } from "./HttpClient" import { IHttpConnectionOptions } from "./IHttpConnectionOptions" -enum ConnectionState { +const enum ConnectionState { Initial, Connecting, Connected, @@ -25,6 +25,8 @@ export class HttpConnection implements IConnection { private options: IHttpConnectionOptions; private startPromise: Promise; + readonly features: any = {}; + constructor(url: string, options: IHttpConnectionOptions = {}) { this.url = url; this.httpClient = options.httpClient || new HttpClient(); @@ -59,7 +61,14 @@ export class HttpConnection implements IConnection { this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports); this.transport.onDataReceived = this.onDataReceived; this.transport.onClosed = e => this.stopConnection(true, e); - await this.transport.connect(this.url); + + let requestedTransferMode = + this.features.transferMode === TransferMode.Binary + ? TransferMode.Binary + : TransferMode.Text; + + this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode); + // only change the state if we were connecting to not overwrite // the state if the connection is already marked as Disconnected this.changeState(ConnectionState.Connecting, ConnectionState.Connected); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts index 96164bc9d3..38698d7f6b 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts @@ -1,13 +1,15 @@ import { ConnectionClosed } from "./Common" import { IConnection } from "./IConnection" -import { TransportType } from "./Transports" +import { TransportType, TransferMode } from "./Transports" import { Subject, Observable } from "./Observable" -export { TransportType } from "./Transports" -export { HttpConnection } from "./HttpConnection" -import { IHubProtocol, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, NegotiationMessage } from "./IHubProtocol"; +import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, NegotiationMessage } from "./IHubProtocol"; import { JsonHubProtocol } from "./JsonHubProtocol"; import { TextMessageFormat } from "./Formatters" +export { TransportType } from "./Transports" +export { HttpConnection } from "./HttpConnection" +export { JsonHubProtocol } from "./JsonHubProtocol" + export class HubConnection { private connection: IConnection; private callbacks: Map void>; @@ -91,10 +93,22 @@ export class HubConnection { } async start(): Promise { + let requestedTransferMode = + (this.protocol.type === ProtocolType.Binary) + ? TransferMode.Binary + : TransferMode.Text; + + this.connection.features.transferMode = requestedTransferMode await this.connection.start(); + var actualTransferMode = this.connection.features.transferMode; + await this.connection.send( TextMessageFormat.write( - JSON.stringify({ protocol: this.protocol.name()}))); + JSON.stringify({ protocol: this.protocol.name}))); + + if (requestedTransferMode === TransferMode.Binary && actualTransferMode === TransferMode.Text) { + this.protocol = new Base64EncodedHubProtocol(this.protocol); + } } stop(): void { @@ -196,3 +210,38 @@ export class HubConnection { }; } } + +class Base64EncodedHubProtocol implements IHubProtocol { + private wrappedProtocol: IHubProtocol; + + constructor(protocol: IHubProtocol) { + this.wrappedProtocol = protocol; + this.name = this.wrappedProtocol.name; + this.type = ProtocolType.Text; + } + + readonly name: string; + readonly type: ProtocolType; + + parseMessages(input: any): HubMessage[] { + // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use + // base64-js module + let s = atob(input); + let payload = new Uint8Array(s.length); + for (let i = 0; i < payload.length; i++) { + payload[i] = s.charCodeAt(i); + } + return this.wrappedProtocol.parseMessages(payload.buffer); + } + + writeMessage(message: HubMessage) { + let payload = new Uint8Array(this.wrappedProtocol.writeMessage(message)); + let s = ""; + for (var i = 0; i < payload.byteLength; i++) { + s += String.fromCharCode(payload[i]); + } + // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use + // base64-js module + return btoa(s); + } +} \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IConnection.ts index d6fa63db39..dee6616196 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IConnection.ts @@ -1,7 +1,9 @@ import { DataReceived, ConnectionClosed } from "./Common" -import { TransportType, ITransport } from "./Transports" +import { TransportType, TransferMode, ITransport } from "./Transports" export interface IConnection { + readonly features: any; + start(): Promise; send(data: any): Promise; stop(): void; diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts index ff1ff3de6e..6f81641380 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol.ts @@ -28,8 +28,14 @@ export interface NegotiationMessage { readonly protocol: string; } +export const enum ProtocolType { + Text = 1, + Binary +} + export interface IHubProtocol { - name(): string; + readonly name: string; + readonly type: ProtocolType; parseMessages(input: any): HubMessage[]; writeMessage(message: HubMessage): any; } \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/JsonHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/JsonHubProtocol.ts index e48c9fc2f2..67158168ee 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/JsonHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/JsonHubProtocol.ts @@ -1,10 +1,11 @@ import { TextMessageFormat } from "./Formatters"; -import { IHubProtocol, HubMessage } from "./IHubProtocol"; +import { IHubProtocol, ProtocolType, HubMessage } from "./IHubProtocol"; export class JsonHubProtocol implements IHubProtocol { - name(): string { - return "json" - } + + readonly name: string = "json"; + + readonly type: ProtocolType = ProtocolType.Text; parseMessages(input: string): HubMessage[] { if (!input) { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts index 812f57cda2..5892cc744f 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/MessagePackHubProtocol.ts @@ -1,11 +1,12 @@ -import { IHubProtocol, MessageType, HubMessage, InvocationMessage, ResultMessage, CompletionMessage } from "./IHubProtocol"; +import { IHubProtocol, ProtocolType, MessageType, HubMessage, InvocationMessage, ResultMessage, CompletionMessage } from "./IHubProtocol"; import { BinaryMessageFormat } from "./Formatters" import * as msgpack5 from "msgpack5" export class MessagePackHubProtocol implements IHubProtocol { - name(): string { - return "messagepack"; - } + + readonly name: string = "messagepack"; + + readonly type: ProtocolType = ProtocolType.Binary; parseMessages(input: ArrayBuffer): HubMessage[] { return BinaryMessageFormat.parse(input).map(m => this.parseMessage(m)); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts index 64267003bc..6dca4289ce 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts @@ -1,5 +1,6 @@ import { DataReceived, TransportClosed } from "./Common" import { IHttpClient } from "./HttpClient" +import { HttpError } from "./HttpError" export enum TransportType { WebSockets, @@ -7,8 +8,13 @@ export enum TransportType { LongPolling } +export const enum TransferMode { + Text = 1, + Binary +} + export interface ITransport { - connect(url: string): Promise; + connect(url: string, requestedTransferMode: TransferMode): Promise; send(data: any): Promise; stop(): void; onDataReceived: DataReceived; @@ -18,16 +24,20 @@ export interface ITransport { export class WebSocketTransport implements ITransport { private webSocket: WebSocket; - connect(url: string, queryString: string = ""): Promise { - return new Promise((resolve, reject) => { + connect(url: string, requestedTransferMode: TransferMode): Promise { + + return new Promise((resolve, reject) => { url = url.replace(/^http/, "ws"); let webSocket = new WebSocket(url); + if (requestedTransferMode == TransferMode.Binary) { + webSocket.binaryType = "arraybuffer"; + } webSocket.onopen = (event: Event) => { console.log(`WebSocket connected to ${url}`); this.webSocket = webSocket; - resolve(); + resolve(requestedTransferMode); }; webSocket.onerror = (event: Event) => { @@ -78,20 +88,19 @@ export class WebSocketTransport implements ITransport { export class ServerSentEventsTransport implements ITransport { private eventSource: EventSource; private url: string; - private queryString: string; private httpClient: IHttpClient; constructor(httpClient: IHttpClient) { this.httpClient = httpClient; } - connect(url: string): Promise { + connect(url: string, requestedTransferMode: TransferMode): Promise { if (typeof (EventSource) === "undefined") { - Promise.reject("EventSource not supported by the browser.") + Promise.reject("EventSource not supported by the browser."); } this.url = url; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let eventSource = new EventSource(this.url); try { @@ -121,7 +130,8 @@ export class ServerSentEventsTransport implements ITransport { eventSource.onopen = () => { console.log(`SSE connected to ${this.url}`); this.eventSource = eventSource; - resolve(); + // SSE is a text protocol + resolve(TransferMode.Text); } } catch (e) { @@ -155,19 +165,22 @@ export class LongPollingTransport implements ITransport { this.httpClient = httpClient; } - connect(url: string): Promise { + connect(url: string, requestedTransferMode: TransferMode): Promise { this.url = url; this.shouldPoll = true; - this.poll(this.url); - return Promise.resolve(); + this.poll(this.url, requestedTransferMode); + return Promise.resolve(requestedTransferMode); } - private poll(url: string): void { + private poll(url: string, transferMode: TransferMode): void { if (!this.shouldPoll) { return; } let pollXhr = new XMLHttpRequest(); + if (transferMode === TransferMode.Binary) { + pollXhr.responseType = "arraybuffer"; + } pollXhr.onload = () => { if (pollXhr.status == 200) { @@ -187,7 +200,7 @@ export class LongPollingTransport implements ITransport { return; } } - this.poll(url); + this.poll(url, transferMode); } else if (this.pollXhr.status == 204) { if (this.onClosed) { @@ -196,7 +209,7 @@ export class LongPollingTransport implements ITransport { } else { if (this.onClosed) { - this.onClosed(new Error(`Status: ${pollXhr.status}, Message: ${pollXhr.responseText}`)); + this.onClosed(new HttpError(pollXhr.statusText, pollXhr.status)); } } }; @@ -209,7 +222,7 @@ export class LongPollingTransport implements ITransport { }; pollXhr.ontimeout = () => { - this.poll(url); + this.poll(url, transferMode); } this.pollXhr = pollXhr; diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js index dccfb511f5..7bb457b646 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js @@ -24,7 +24,15 @@ gulp.task('browserify-client', ['compile-ts-client'], () => { .pipe(gulp.dest(clientOutDir + '/../browser')); }); +gulp.task('browserify-msgpackprotocol', ['compile-ts-client'], () => { + return browserify(clientOutDir + '/MessagePackHubProtocol.js', {standalone: 'signalRMsgPack'}) + .bundle() + .pipe(source('signalr-msgpackprotocol.js')) + .pipe(gulp.dest(clientOutDir + '/../browser')); +}); -gulp.task('build-ts-client', ['clean', 'compile-ts-client', 'browserify-client']); +gulp.task('browserify', [ 'browserify-client', 'browserify-msgpackprotocol']); + +gulp.task('build-ts-client', ['clean', 'compile-ts-client', 'browserify']); gulp.task('default', ['build-ts-client']); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj index 5e24866e29..caffe826be 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj @@ -26,7 +26,8 @@ - + diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html index 94f5545107..a888257b17 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html @@ -8,7 +8,8 @@ - + + diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/common.js b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/common.js index 221babe7e4..92660ed7a0 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/common.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/common.js @@ -5,5 +5,18 @@ function eachTransport(action) { signalR.TransportType.WebSockets, signalR.TransportType.ServerSentEvents, signalR.TransportType.LongPolling ]; - transportTypes.forEach(t => action(t)); + transportTypes.forEach(t => action(t)); +} + +function eachTransportAndProtocol(action) { + let transportTypes = [ + signalR.TransportType.WebSockets, + signalR.TransportType.ServerSentEvents, + signalR.TransportType.LongPolling ]; + let protocols = [ + new signalR.JsonHubProtocol(), + new signalRMsgPack.MessagePackHubProtocol() + ]; + transportTypes.forEach(t => + protocols.forEach(p => action(t, p))); } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js index 5f6296d14d..5d13aa36a8 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js @@ -1,11 +1,12 @@ const TESTHUBENDPOINT_URL = `http://${document.location.host}/testhub`; describe('hubConnection', () => { - eachTransport(transportType => { - describe(`${signalR.TransportType[transportType]} transport`, () => { + eachTransportAndProtocol((transportType, protocol) => { + describe(`${protocol.name} over ${signalR.TransportType[transportType]} transport`, () => { it(`can invoke server method and receive result`, done => { const message = "Hi"; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); + let hubConnection = new signalR.HubConnection( + new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), protocol); hubConnection.onClosed = error => { expect(error).toBe(undefined); done(); @@ -31,7 +32,8 @@ describe('hubConnection', () => { }); it(`can stream server method and receive result`, done => { - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); + let hubConnection = new signalR.HubConnection( + new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), protocol); hubConnection.onClosed = error => { expect(error).toBe(undefined); done(); @@ -63,7 +65,8 @@ describe('hubConnection', () => { it(`rethrows an exception from the server when invoking`, done => { const errorMessage = "An error occurred."; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); + let hubConnection = new signalR.HubConnection( + new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), protocol); hubConnection.start() .then(() => { @@ -90,7 +93,8 @@ describe('hubConnection', () => { it(`rethrows an exception from the server when streaming`, done => { const errorMessage = "An error occurred."; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); + let hubConnection = new signalR.HubConnection( + new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), protocol); hubConnection.start() .then(() => { @@ -116,7 +120,8 @@ describe('hubConnection', () => { }); it(`can receive server calls`, done => { - let client = new signalR.HubConnection(new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType })); + let client = new signalR.HubConnection( + new signalR.HttpConnection(TESTHUBENDPOINT_URL, { transport: transportType }), protocol); const message = "Hello SignalR"; let callbackPromise = new Promise((resolve, reject) => { @@ -141,22 +146,23 @@ describe('hubConnection', () => { done(); }); }); - }); - it(`over ${signalR.TransportType[transportType]} closed with error if hub cannot be created`, done =>{ - let errorRegex = { - WebSockets: "1011", // Message is browser specific (e.g. 'Websocket closed with status code: 1011') - LongPolling: "Status: 500", - ServerSentEvents: "Error occurred" - }; + it(`closed with error if hub cannot be created`, done => { + let errorRegex = { + WebSockets: "1011", // Message is browser specific (e.g. 'Websocket closed with status code: 1011') + LongPolling: "Internal Server Error", + ServerSentEvents: "Error occurred" + }; - let hubConnection = new signalR.HubConnection(new signalR.HttpConnection(`http://${document.location.host}/uncreatable`, { transport: transportType })); + let hubConnection = new signalR.HubConnection( + new signalR.HttpConnection(`http://${document.location.host}/uncreatable`, { transport: transportType }), protocol); - hubConnection.onClosed = error => { - expect(error).toMatch(errorRegex[signalR.TransportType[transportType]]); - done(); - } - hubConnection.start(); + hubConnection.onClosed = error => { + expect(error.message).toMatch(errorRegex[signalR.TransportType[transportType]]); + done(); + } + hubConnection.start(); + }); }); }); }); diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs index 9b62789983..7aa84abab6 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/MessagePackHubProtocol.cs @@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol completionMessage.HasResult ? NonVoidResult : VoidResult; - packer.PackArrayHeader(3 + resultKind != VoidResult ? 1 : 0); + packer.PackArrayHeader(3 + (resultKind != VoidResult ? 1 : 0)); packer.Pack(CompletionMessageType); packer.PackString(completionMessage.InvocationId); packer.Pack(resultKind);