Introducing modules for the ts client

This commit is contained in:
moozzyk 2016-11-16 15:13:12 -08:00
parent 1b59fc6f80
commit 2039a18971
19 changed files with 404 additions and 350 deletions

2
.gitignore vendored
View File

@ -35,7 +35,7 @@ node_modules/
*.nuget.props *.nuget.props
*.nuget.targets *.nuget.targets
autobahnreports/ autobahnreports/
signalr-client.js
site.min.css site.min.css
.idea/ .idea/
.vscode/ .vscode/
signalr-client/

View File

@ -6,19 +6,27 @@
"directories": { "directories": {
"test": "test" "test": "test"
}, },
"scripts": {}, "scripts": {
"gulp": "gulp"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/aspnet/SignalR.git" "url": "git+https://github.com/aspnet/SignalR.git"
}, },
"author": "Microsoft", "author": "Microsoft",
"license": "MIT", "license": "Apache-2.0",
"bugs": { "bugs": {
"url": "https://github.com/aspnet/SignalR/issues" "url": "https://github.com/aspnet/SignalR/issues"
}, },
"homepage": "https://github.com/aspnet/SignalR#readme", "homepage": "https://github.com/aspnet/SignalR#readme",
"devDependencies": { "devDependencies": {
"browserify": "^13.1.1",
"del": "^2.2.2",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"jasmine": "^2.5.2" "gulp-typescript": "^3.1.3",
"jasmine": "^2.5.2",
"typescript": "^2.0.10",
"vinyl-source-stream": "^1.1.0",
"yargs": "^6.4.0"
} }
} }

View File

@ -2,10 +2,19 @@
ViewData["Title"] = "Chat"; ViewData["Title"] = "Chat";
} }
<script src="~/js/signalr-client.js"></script> <div id="chat-area">
<ul id="messages"></ul>
<ul id="users"></ul>
<div class="clear">
</div>
<form id="sendmessage" action="#">
<input type="text" id="new-message" />
<input type="submit" id="send" value="Send" class="send" />
</form>
</div>
<script src="lib/signalr-client/signalr-client.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { let connection = new signalR.RpcConnection(`http://${document.location.host}/chat`, 'formatType=json&format=text');
let connection = new RpcConnection(`http://${document.location.host}/chat`, 'formatType=json&format=text');
connection.on('Send', function (message) { connection.on('Send', function (message) {
var child = document.createElement('li'); var child = document.createElement('li');
@ -27,16 +36,4 @@
event.preventDefault(); event.preventDefault();
}); });
});
</script> </script>
<div id="chat-area">
<ul id="messages"></ul>
<ul id="users"></ul>
<div class="clear">
</div>
<form id="sendmessage" action="#">
<input type="text" id="new-message" />
<input type="submit" id="send" value="Send" class="send" />
</form>
</div>

View File

@ -91,7 +91,9 @@
}, },
"scripts": { "scripts": {
"precompile": [ "dotnet bundle", "tsc --project ../../src/Microsoft.AspNetCore.SignalR.Client.TS/ --out wwwroot/js/signalr-client.js" ], "precompile": [ "dotnet bundle",
"npm install",
"npm run gulp -- --gulpfile %project:Directory%/../../src/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js bundle-client --bundleOutDir %project:Directory%/wwwroot/lib/signalr-client/" ],
"prepublish": [ "bower install" ], "prepublish": [ "bower install" ],
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
} }

View File

@ -1,5 +1,4 @@
/// <autosync enabled="true" /> /// <autosync enabled="true" />
/// <reference path="js/signalr-client.js" />
/// <reference path="js/site.js" /> /// <reference path="js/site.js" />
/// <reference path="lib/bootstrap/dist/js/bootstrap.js" /> /// <reference path="lib/bootstrap/dist/js/bootstrap.js" />
/// <reference path="lib/jquery/dist/jquery.js" /> /// <reference path="lib/jquery/dist/jquery.js" />

View File

@ -44,7 +44,9 @@
}, },
"scripts": { "scripts": {
"precompile": [ "tsc --project ../../src/Microsoft.AspNetCore.SignalR.Client.TS/ --out wwwroot/js/signalr-client.js" ], "precompile": [
"npm install",
"npm run gulp -- --gulpfile %project:Directory%/../../src/Microsoft.AspNetCore.SignalR.Client.TS/gulpfile.js bundle-client --bundleOutDir %project:Directory%/wwwroot/lib/signalr-client/" ],
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
} }
} }

View File

@ -1 +0,0 @@
js/

View File

@ -3,7 +3,55 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title></title> <title></title>
<script src="js/signalr-client.js"></script> </head>
<body>
<h1 id="head1"></h1>
<div>
<select id="formatType">
<option value="json">json</option>
<option value="line">line</option>
</select>
<input type="button" id="connect" value="Connect" />
<input type="button" id="disconnect" value="Disconnect" />
</div>
<h4>To Everybody</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="msg" placeholder="Type a message, name or group" />
<input type="button" id="broadcast" class="btn" value="Broadcast" />
<input type="button" id="broadcast-exceptme" class="btn" value="Broadcast (All Except Me)" />
<input type="button" id="join" class="btn" value="Enter Name" />
<input type="button" id="join-group" class="btn" value="Join Group" />
<input type="button" id="leave-group" class="btn" value="Leave Group" />
</div>
</form>
<h4>To Me</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="me" placeholder="Type a message" />
<input type="button" id="send" class="btn" value="Send to me" />
</div>
</form>
<h4>Private Message</h4>
<form class="form-inline">
<div class="input-prepend input-append">
<input type="text" name="message" id="message" placeholder="Type a message" />
<input type="text" name="user" id="target" placeholder="Type a user or group name" />
<input type="button" id="privatemsg" class="btn" value="Send to user" />
<input type="button" id="groupmsg" class="btn" value="Send to group" />
</div>
</form>
<ul id="messages"></ul>
</body>
</html>
<script src="lib/signalr-client/signalr-client.js"></script>
<script> <script>
var isConnected = false; var isConnected = false;
function getParameterByName(name, url) { function getParameterByName(name, url) {
@ -47,12 +95,20 @@
return document.getElementById(id).value; return document.getElementById(id).value;
} }
document.addEventListener('DOMContentLoaded', () => { function addLine(line, color) {
var child = document.createElement('li');
if (color) {
child.style.color = color;
}
child.innerText = line;
document.getElementById('messages').appendChild(child);
}
let transport = getParameterByName('transport') || 'webSockets'; let transport = getParameterByName('transport') || 'webSockets';
document.getElementById('head1').innerHTML = transport; document.getElementById('head1').innerHTML = transport;
let connection = new RpcConnection(`http://${document.location.host}/hubs`, 'formatType=json&format=text'); let connection = new signalR.RpcConnection(`http://${document.location.host}/hubs`, 'formatType=json&format=text');
connection.on('Send', msg => { connection.on('Send', msg => {
addLine(msg); addLine(msg);
}); });
@ -102,62 +158,5 @@
let message = getText('message'); let message = getText('message');
invoke(connection, 'SocketsSample.Hubs.Chat.SendToGroup', groupName, message); invoke(connection, 'SocketsSample.Hubs.Chat.SendToGroup', groupName, message);
}); });
});
function addLine(line, color) {
var child = document.createElement('li');
if (color) {
child.style.color = color;
}
child.innerText = line;
document.getElementById('messages').appendChild(child);
}
</script> </script>
</head>
<body>
<h1 id="head1"></h1>
<div>
<select id="formatType">
<option value="json">json</option>
<option value="line">line</option>
</select>
<input type="button" id="connect" value="Connect" />
<input type="button" id="disconnect" value="Disconnect" />
</div>
<h4>To Everybody</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="msg" placeholder="Type a message, name or group" />
<input type="button" id="broadcast" class="btn" value="Broadcast" />
<input type="button" id="broadcast-exceptme" class="btn" value="Broadcast (All Except Me)" />
<input type="button" id="join" class="btn" value="Enter Name" />
<input type="button" id="join-group" class="btn" value="Join Group" />
<input type="button" id="leave-group" class="btn" value="Leave Group" />
</div>
</form>
<h4>To Me</h4>
<form class="form-inline">
<div class="input-append">
<input type="text" id="me" placeholder="Type a message" />
<input type="button" id="send" class="btn" value="Send to me" />
</div>
</form>
<h4>Private Message</h4>
<form class="form-inline">
<div class="input-prepend input-append">
<input type="text" name="message" id="message" placeholder="Type a message" />
<input type="text" name="user" id="target" placeholder="Type a user or group name" />
<input type="button" id="privatemsg" class="btn" value="Send to user" />
<input type="button" id="groupmsg" class="btn" value="Send to group" />
</div>
</form>
<ul id="messages"></ul>
</body>
</html>

View File

@ -1,3 +1,5 @@
import { ITransport, WebSocketTransport, ServerSentEventsTransport, LongPollingTransport } from "./Transports"
import { HttpClient } from "./HttpClient"
enum ConnectionState { enum ConnectionState {
Disconnected, Disconnected,
@ -5,7 +7,7 @@ enum ConnectionState {
Connected Connected
} }
class Connection { export class Connection {
private connectionState: ConnectionState; private connectionState: ConnectionState;
private url: string; private url: string;
private queryString: string; private queryString: string;

View File

@ -1,4 +1,4 @@
class HttpClient { export class HttpClient {
get(url: string): Promise<string> { get(url: string): Promise<string> {
return this.xhr("GET", url); return this.xhr("GET", url);
} }

View File

@ -1,7 +0,0 @@
interface ITransport {
connect(url: string, queryString: string): Promise<void>;
send(data: any): Promise<void>;
stop(): void;
onDataReceived: DataReceived;
onError: ErrorHandler;
}

View File

@ -1,70 +0,0 @@
class LongPollingTransport implements ITransport {
private url: string;
private queryString: string;
private pollXhr: XMLHttpRequest;
connect(url: string, queryString: string): Promise<void> {
this.url = url;
this.queryString = queryString;
this.poll(url + "/poll?" + this.queryString)
return Promise.resolve();
}
private poll(url: string): void {
let thisLongPollingTransport = this;
let pollXhr = new XMLHttpRequest();
pollXhr.onload = () => {
if (pollXhr.status == 200) {
if (thisLongPollingTransport.onDataReceived) {
thisLongPollingTransport.onDataReceived(pollXhr.response);
}
thisLongPollingTransport.poll(url);
}
else if (this.pollXhr.status == 204) {
// TODO: closed event?
}
else {
if (thisLongPollingTransport.onError) {
thisLongPollingTransport.onError({
status: pollXhr.status,
statusText: pollXhr.statusText
});
}
}
};
pollXhr.onerror = () => {
if (thisLongPollingTransport.onError) {
thisLongPollingTransport.onError({
status: pollXhr.status,
statusText: pollXhr.statusText
});
}
};
pollXhr.ontimeout = () => {
thisLongPollingTransport.poll(url);
}
this.pollXhr = pollXhr;
this.pollXhr.open("GET", url, true);
// TODO: consider making timeout configurable
this.pollXhr.timeout = 110000;
this.pollXhr.send();
}
send(data: any): Promise<void> {
return new HttpClient().post(this.url + "/send?" + this.queryString, data);
}
stop(): void {
if (this.pollXhr) {
this.pollXhr.abort();
this.pollXhr = null;
}
}
onDataReceived: DataReceived;
onError: ErrorHandler;
}

View File

@ -1,3 +1,5 @@
import { Connection } from "./Connection"
interface InvocationDescriptor { interface InvocationDescriptor {
readonly Id: string; readonly Id: string;
readonly Method: string; readonly Method: string;
@ -10,7 +12,7 @@ interface InvocationResultDescriptor {
readonly Result: any; readonly Result: any;
} }
class RpcConnection { export class RpcConnection {
private connection: Connection; private connection: Connection;
private callbacks: Map<string, (any) => void>; private callbacks: Map<string, (any) => void>;
private methods: Map<string, (...args:any[]) => void>; private methods: Map<string, (...args:any[]) => void>;

View File

@ -1,61 +0,0 @@
// TODO: need EvenSource typings
class ServerSentEventsTransport implements ITransport {
private eventSource: EventSource;
private url: string;
private queryString: string;
connect(url: string, queryString: string): Promise<void> {
if (typeof (EventSource) === "undefined") {
Promise.reject("EventSource not supported by the browser.")
}
this.queryString = queryString;
this.url = url;
let tmp = `${this.url}/sse?${this.queryString}`;
return new Promise((resolve, reject) => {
let eventSource = new EventSource(`${this.url}/sse?${this.queryString}`);
try {
let thisEventSourceTransport = this;
eventSource.onmessage = (e: MessageEvent) => {
if (thisEventSourceTransport.onDataReceived) {
thisEventSourceTransport.onDataReceived(e.data);
}
};
eventSource.onerror = (e: Event) => {
reject();
// don't report an error if the transport did not start successfully
if (thisEventSourceTransport.eventSource && thisEventSourceTransport.onError) {
thisEventSourceTransport.onError(e);
}
}
eventSource.onopen = () => {
thisEventSourceTransport.eventSource = eventSource;
resolve();
}
}
catch (e) {
return Promise.reject(e);
}
});
}
send(data: any): Promise<void> {
return new HttpClient().post(this.url + "/send?" + this.queryString, data);
}
stop(): void {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
onDataReceived: DataReceived;
onError: ErrorHandler;
}

View File

@ -0,0 +1,199 @@
import { HttpClient } from "./HttpClient"
export interface ITransport {
connect(url: string, queryString: string): Promise<void>;
send(data: any): Promise<void>;
stop(): void;
onDataReceived: DataReceived;
onError: ErrorHandler;
}
export class WebSocketTransport implements ITransport {
private webSocket: WebSocket;
connect(url: string, queryString: string = ""): Promise<void> {
return new Promise((resolve, reject) => {
url = url.replace(/^http/, "ws");
let connectUrl = url + "/ws?" + queryString;
let webSocket = new WebSocket(connectUrl);
let thisWebSocketTransport = this;
webSocket.onopen = (event: Event) => {
console.log(`WebSocket connected to ${connectUrl}`);
thisWebSocketTransport.webSocket = webSocket;
resolve();
};
webSocket.onerror = (event: Event) => {
reject();
};
webSocket.onmessage = (message: MessageEvent) => {
console.log(`(WebSockets transport) data received: ${message.data}`);
if (thisWebSocketTransport.onDataReceived) {
thisWebSocketTransport.onDataReceived(message.data);
}
}
webSocket.onclose = (event: CloseEvent) => {
// webSocket will be null if the transport did not start successfully
if (thisWebSocketTransport.webSocket && event.wasClean === false) {
if (thisWebSocketTransport.onError) {
thisWebSocketTransport.onError(event);
}
}
}
});
}
send(data: any): Promise<void> {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(data);
return Promise.resolve();
}
return Promise.reject("WebSocket is not in OPEN state");
}
stop(): void {
if (this.webSocket) {
this.webSocket.close();
this.webSocket = null;
}
}
onDataReceived: DataReceived;
onError: ErrorHandler;
}
export class ServerSentEventsTransport implements ITransport {
private eventSource: EventSource;
private url: string;
private queryString: string;
connect(url: string, queryString: string): Promise<void> {
if (typeof (EventSource) === "undefined") {
Promise.reject("EventSource not supported by the browser.")
}
this.queryString = queryString;
this.url = url;
let tmp = `${this.url}/sse?${this.queryString}`;
return new Promise((resolve, reject) => {
let eventSource = new EventSource(`${this.url}/sse?${this.queryString}`);
try {
let thisEventSourceTransport = this;
eventSource.onmessage = (e: MessageEvent) => {
if (thisEventSourceTransport.onDataReceived) {
thisEventSourceTransport.onDataReceived(e.data);
}
};
eventSource.onerror = (e: Event) => {
reject();
// don't report an error if the transport did not start successfully
if (thisEventSourceTransport.eventSource && thisEventSourceTransport.onError) {
thisEventSourceTransport.onError(e);
}
}
eventSource.onopen = () => {
thisEventSourceTransport.eventSource = eventSource;
resolve();
}
}
catch (e) {
return Promise.reject(e);
}
});
}
send(data: any): Promise<void> {
return new HttpClient().post(this.url + "/send?" + this.queryString, data);
}
stop(): void {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
onDataReceived: DataReceived;
onError: ErrorHandler;
}
export class LongPollingTransport implements ITransport {
private url: string;
private queryString: string;
private pollXhr: XMLHttpRequest;
connect(url: string, queryString: string): Promise<void> {
this.url = url;
this.queryString = queryString;
this.poll(url + "/poll?" + this.queryString)
return Promise.resolve();
}
private poll(url: string): void {
let thisLongPollingTransport = this;
let pollXhr = new XMLHttpRequest();
pollXhr.onload = () => {
if (pollXhr.status == 200) {
if (thisLongPollingTransport.onDataReceived) {
thisLongPollingTransport.onDataReceived(pollXhr.response);
}
thisLongPollingTransport.poll(url);
}
else if (this.pollXhr.status == 204) {
// TODO: closed event?
}
else {
if (thisLongPollingTransport.onError) {
thisLongPollingTransport.onError({
status: pollXhr.status,
statusText: pollXhr.statusText
});
}
}
};
pollXhr.onerror = () => {
if (thisLongPollingTransport.onError) {
thisLongPollingTransport.onError({
status: pollXhr.status,
statusText: pollXhr.statusText
});
}
};
pollXhr.ontimeout = () => {
thisLongPollingTransport.poll(url);
}
this.pollXhr = pollXhr;
this.pollXhr.open("GET", url, true);
// TODO: consider making timeout configurable
this.pollXhr.timeout = 110000;
this.pollXhr.send();
}
send(data: any): Promise<void> {
return new HttpClient().post(this.url + "/send?" + this.queryString, data);
}
stop(): void {
if (this.pollXhr) {
this.pollXhr.abort();
this.pollXhr = null;
}
}
onDataReceived: DataReceived;
onError: ErrorHandler;
}

View File

@ -1,58 +0,0 @@
class WebSocketTransport implements ITransport {
private webSocket: WebSocket;
connect(url: string, queryString: string = ""): Promise<void> {
return new Promise((resolve, reject) => {
url = url.replace(/^http/, "ws");
let connectUrl = url + "/ws?" + queryString;
let webSocket = new WebSocket(connectUrl);
let thisWebSocketTransport = this;
webSocket.onopen = (event: Event) => {
console.log(`WebSocket connected to ${connectUrl}`);
thisWebSocketTransport.webSocket = webSocket;
resolve();
};
webSocket.onerror = (event: Event) => {
reject();
};
webSocket.onmessage = (message: MessageEvent) => {
console.log(`(WebSockets transport) data received: ${message.data}`);
if (thisWebSocketTransport.onDataReceived) {
thisWebSocketTransport.onDataReceived(message.data);
}
}
webSocket.onclose = (event: CloseEvent) => {
// webSocket will be null if the transport did not start successfully
if (thisWebSocketTransport.webSocket && event.wasClean === false) {
if (thisWebSocketTransport.onError) {
thisWebSocketTransport.onError(event);
}
}
}
});
}
send(data: any): Promise<void> {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(data);
return Promise.resolve();
}
return Promise.reject("WebSocket is not in OPEN state");
}
stop(): void {
if (this.webSocket) {
this.webSocket.close();
this.webSocket = null;
}
}
onDataReceived: DataReceived;
onError: ErrorHandler;
}

View File

@ -0,0 +1,40 @@
const gulp = require('gulp');
const browserify = require('browserify');
const ts = require('gulp-typescript');
const source = require('vinyl-source-stream');
const del = require('del');
const argv = require('yargs').argv;
const tsProject = ts.createProject('./tsconfig.json');
const clientOutDir = tsProject.options.outDir;
gulp.task('clean', () => {
return del([clientOutDir + '/..'], { force: true });
});
gulp.task('compile-ts-client', () => {
return tsProject.src()
.pipe(tsProject())
.pipe(gulp.dest(clientOutDir));
});
gulp.task('browserify-client', ['compile-ts-client'], () => {
return browserify(clientOutDir + '/RpcConnection.js', {standalone: 'signalR'})
.bundle()
.pipe(source('signalr-client.js'))
.pipe(gulp.dest(clientOutDir + '/../signalr-client-bundle'));
});
gulp.task('build-ts-client', ['clean', 'compile-ts-client', 'browserify-client']);
gulp.task('bundle-client', ['build-ts-client'], () => {
if (!argv.bundleOutDir) {
console.log('Use \'--bundleOutDir\' option to specify the target file for the bundled client.');
}
else {
return gulp.src(clientOutDir + '/../signalr-client-bundle/signalr-client.js')
.pipe(gulp.dest(argv.bundleOutDir));
}
});
gulp.task('default', ['build-ts-client']);

View File

@ -1,8 +1,9 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"module": "umd",
"target": "es6", "target": "es6",
"out": "../../samples/SocketsSample/wwwroot/js/signalr-client.js" "outDir": "../../artifacts/lib/signalr-client-modules"
}, },
"include": [ "*.ts" ] "include": [ "*.ts" ]
} }

View File

@ -39,7 +39,7 @@
}, },
"scripts": { "scripts": {
"precompile": [ "npm install", "gulp copy-jasmine"], "precompile": [ "npm install", "npm run gulp -- --gulpfile %project:Directory%/gulpfile.js copy-jasmine"],
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
} }
} }