@aspnet/signalr
Advanced tools
Comparing version 3.0.0-preview3-19153-02 to 3.0.0-preview4-19216-03
@@ -17,10 +17,4 @@ "use strict"; | ||
var HttpClient_1 = require("./HttpClient"); | ||
var NodeHttpClient_1 = require("./NodeHttpClient"); | ||
var XhrHttpClient_1 = require("./XhrHttpClient"); | ||
var nodeHttpClientModule; | ||
if (typeof XMLHttpRequest === "undefined") { | ||
// In order to ignore the dynamic require in webpack builds we need to do this magic | ||
// @ts-ignore: TS doesn't know about these names | ||
var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; | ||
nodeHttpClientModule = requireFunc("./NodeHttpClient"); | ||
} | ||
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */ | ||
@@ -35,7 +29,4 @@ var DefaultHttpClient = /** @class */ (function (_super) { | ||
} | ||
else if (typeof nodeHttpClientModule !== "undefined") { | ||
_this.httpClient = new nodeHttpClientModule.NodeHttpClient(logger); | ||
} | ||
else { | ||
throw new Error("No HttpClient could be created."); | ||
_this.httpClient = new NodeHttpClient_1.NodeHttpClient(logger); | ||
} | ||
@@ -42,0 +33,0 @@ return _this; |
@@ -84,3 +84,4 @@ "use strict"; | ||
this.httpClient = options.httpClient || new DefaultHttpClient_1.DefaultHttpClient(this.logger); | ||
this.connectionState = 2 /* Disconnected */; | ||
this.connectionState = "Disconnected" /* Disconnected */; | ||
this.connectionStarted = false; | ||
this.options = options; | ||
@@ -91,15 +92,44 @@ this.onreceive = null; | ||
HttpConnection.prototype.start = function (transferFormat) { | ||
transferFormat = transferFormat || ITransport_1.TransferFormat.Binary; | ||
Utils_1.Arg.isIn(transferFormat, ITransport_1.TransferFormat, "transferFormat"); | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Starting connection with transfer format '" + ITransport_1.TransferFormat[transferFormat] + "'."); | ||
if (this.connectionState !== 2 /* Disconnected */) { | ||
return Promise.reject(new Error("Cannot start a connection that is not in the 'Disconnected' state.")); | ||
} | ||
this.connectionState = 0 /* Connecting */; | ||
this.startPromise = this.startInternal(transferFormat); | ||
return this.startPromise; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var message, message; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
transferFormat = transferFormat || ITransport_1.TransferFormat.Binary; | ||
Utils_1.Arg.isIn(transferFormat, ITransport_1.TransferFormat, "transferFormat"); | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Starting connection with transfer format '" + ITransport_1.TransferFormat[transferFormat] + "'."); | ||
if (this.connectionState !== "Disconnected" /* Disconnected */) { | ||
return [2 /*return*/, Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."))]; | ||
} | ||
this.connectionState = "Connecting " /* Connecting */; | ||
this.startInternalPromise = this.startInternal(transferFormat); | ||
return [4 /*yield*/, this.startInternalPromise]; | ||
case 1: | ||
_a.sent(); | ||
if (!(this.connectionState === "Disconnecting" /* Disconnecting */)) return [3 /*break*/, 3]; | ||
message = "Failed to start the HttpConnection before stop() was called."; | ||
this.logger.log(ILogger_1.LogLevel.Error, message); | ||
// We cannot await stopPromise inside startInternal since stopInternal awaits the startInternalPromise. | ||
return [4 /*yield*/, this.stopPromise]; | ||
case 2: | ||
// We cannot await stopPromise inside startInternal since stopInternal awaits the startInternalPromise. | ||
_a.sent(); | ||
return [2 /*return*/, Promise.reject(new Error(message))]; | ||
case 3: | ||
if (this.connectionState !== "Connected" /* Connected */) { | ||
message = "HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!"; | ||
this.logger.log(ILogger_1.LogLevel.Error, message); | ||
return [2 /*return*/, Promise.reject(new Error(message))]; | ||
} | ||
_a.label = 4; | ||
case 4: | ||
this.connectionStarted = true; | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HttpConnection.prototype.send = function (data) { | ||
if (this.connectionState !== 1 /* Connected */) { | ||
throw new Error("Cannot send data if the connection is not in the 'Connected' State."); | ||
if (this.connectionState !== "Connected" /* Connected */) { | ||
return Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")); | ||
} | ||
@@ -111,7 +141,38 @@ // Transport will not be null if state is connected | ||
return __awaiter(this, void 0, void 0, function () { | ||
var e_1; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
this.connectionState = 2 /* Disconnected */; | ||
if (this.connectionState === "Disconnected" /* Disconnected */) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Call to HttpConnection.stop(" + error + ") ignored because the connection is already in the disconnected state."); | ||
return [2 /*return*/, Promise.resolve()]; | ||
} | ||
if (this.connectionState === "Disconnecting" /* Disconnecting */) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Call to HttpConnection.stop(" + error + ") ignored because the connection is already in the disconnecting state."); | ||
return [2 /*return*/, this.stopPromise]; | ||
} | ||
this.connectionState = "Disconnecting" /* Disconnecting */; | ||
this.stopPromise = new Promise(function (resolve) { | ||
// Don't complete stop() until stopConnection() completes. | ||
_this.stopPromiseResolver = resolve; | ||
}); | ||
// stopInternal should never throw so just observe it. | ||
return [4 /*yield*/, this.stopInternal(error)]; | ||
case 1: | ||
// stopInternal should never throw so just observe it. | ||
_a.sent(); | ||
return [4 /*yield*/, this.stopPromise]; | ||
case 2: | ||
_a.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HttpConnection.prototype.stopInternal = function (error) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var e_1, e_2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
// Set error as soon as possible otherwise there is a race between | ||
@@ -124,3 +185,3 @@ // the transport closing and providing an error and the error from a close message | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.startPromise]; | ||
return [4 /*yield*/, this.startInternalPromise]; | ||
case 2: | ||
@@ -133,9 +194,23 @@ _a.sent(); | ||
case 4: | ||
if (!this.transport) return [3 /*break*/, 6]; | ||
if (!this.transport) return [3 /*break*/, 9]; | ||
_a.label = 5; | ||
case 5: | ||
_a.trys.push([5, 7, , 8]); | ||
return [4 /*yield*/, this.transport.stop()]; | ||
case 5: | ||
case 6: | ||
_a.sent(); | ||
return [3 /*break*/, 8]; | ||
case 7: | ||
e_2 = _a.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Error, "HttpConnection.transport.stop() threw error '" + e_2 + "'."); | ||
this.stopConnection(); | ||
return [3 /*break*/, 8]; | ||
case 8: | ||
this.transport = undefined; | ||
_a.label = 6; | ||
case 6: return [2 /*return*/]; | ||
return [3 /*break*/, 10]; | ||
case 9: | ||
this.logger.log(ILogger_1.LogLevel.Debug, "HttpConnection.transport is undefined in HttpConnection.stop() because start() failed."); | ||
this.stopConnection(); | ||
_a.label = 10; | ||
case 10: return [2 /*return*/]; | ||
} | ||
@@ -147,3 +222,3 @@ }); | ||
return __awaiter(this, void 0, void 0, function () { | ||
var url, negotiateResponse, redirects, _loop_1, this_1, state_1, e_2; | ||
var url, negotiateResponse, redirects, _loop_1, this_1, e_3; | ||
var _this = this; | ||
@@ -170,3 +245,3 @@ return __generator(this, function (_a) { | ||
return [3 /*break*/, 4]; | ||
case 3: throw Error("Negotiation can only be skipped when using the WebSocket transport directly."); | ||
case 3: throw new Error("Negotiation can only be skipped when using the WebSocket transport directly."); | ||
case 4: return [3 /*break*/, 11]; | ||
@@ -184,10 +259,10 @@ case 5: | ||
// the user tries to stop the connection when it is being started | ||
if (this_1.connectionState === 2 /* Disconnected */) { | ||
return [2 /*return*/, { value: void 0 }]; | ||
if (this_1.connectionState === "Disconnecting" /* Disconnecting */ || this_1.connectionState === "Disconnected" /* Disconnected */) { | ||
throw new Error("The connection was stopped during negotiation."); | ||
} | ||
if (negotiateResponse.error) { | ||
throw Error(negotiateResponse.error); | ||
throw new Error(negotiateResponse.error); | ||
} | ||
if (negotiateResponse.ProtocolVersion) { | ||
throw Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); | ||
throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); | ||
} | ||
@@ -210,5 +285,3 @@ if (negotiateResponse.url) { | ||
case 7: | ||
state_1 = _a.sent(); | ||
if (typeof state_1 === "object") | ||
return [2 /*return*/, state_1.value]; | ||
_a.sent(); | ||
_a.label = 8; | ||
@@ -220,4 +293,5 @@ case 8: | ||
if (redirects === MAX_REDIRECTS && negotiateResponse.url) { | ||
throw Error("Negotiate redirection limit exceeded."); | ||
throw new Error("Negotiate redirection limit exceeded."); | ||
} | ||
this.connectionId = negotiateResponse.connectionId; | ||
return [4 /*yield*/, this.createTransport(url, this.options.transport, negotiateResponse, transferFormat)]; | ||
@@ -233,12 +307,15 @@ case 10: | ||
this.transport.onclose = function (e) { return _this.stopConnection(e); }; | ||
// only change the state if we were connecting to not overwrite | ||
// the state if the connection is already marked as Disconnected | ||
this.changeState(0 /* Connecting */, 1 /* Connected */); | ||
if (this.connectionState === "Connecting " /* Connecting */) { | ||
// Ensure the connection transitions to the connected state prior to completing this.startInternalPromise. | ||
// start() will handle the case when stop was called and startInternal exits still in the disconnecting state. | ||
this.logger.log(ILogger_1.LogLevel.Debug, "The HttpConnection connected successfully."); | ||
this.connectionState = "Connected" /* Connected */; | ||
} | ||
return [3 /*break*/, 13]; | ||
case 12: | ||
e_2 = _a.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Error, "Failed to start the connection: " + e_2); | ||
this.connectionState = 2 /* Disconnected */; | ||
e_3 = _a.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Error, "Failed to start the connection: " + e_3); | ||
this.connectionState = "Disconnected" /* Disconnected */; | ||
this.transport = undefined; | ||
throw e_2; | ||
return [2 /*return*/, Promise.reject(e_3)]; | ||
case 13: return [2 /*return*/]; | ||
@@ -251,3 +328,3 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a, headers, token, negotiateUrl, response, e_3; | ||
var _a, headers, token, negotiateUrl, response, e_4; | ||
return __generator(this, function (_b) { | ||
@@ -279,9 +356,9 @@ switch (_b.label) { | ||
if (response.statusCode !== 200) { | ||
throw Error("Unexpected status code returned from negotiate " + response.statusCode); | ||
return [2 /*return*/, Promise.reject(new Error("Unexpected status code returned from negotiate " + response.statusCode))]; | ||
} | ||
return [2 /*return*/, JSON.parse(response.content)]; | ||
case 5: | ||
e_3 = _b.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Error, "Failed to complete negotiation with the server: " + e_3); | ||
throw e_3; | ||
e_4 = _b.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Error, "Failed to complete negotiation with the server: " + e_4); | ||
return [2 /*return*/, Promise.reject(e_4)]; | ||
case 6: return [2 /*return*/]; | ||
@@ -300,3 +377,3 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var connectUrl, transportExceptions, transports, _i, transports_1, endpoint, transport, ex_1; | ||
var connectUrl, transportExceptions, transports, _i, transports_1, endpoint, transport, ex_1, message; | ||
return __generator(this, function (_a) { | ||
@@ -312,5 +389,2 @@ switch (_a.label) { | ||
_a.sent(); | ||
// only change the state if we were connecting to not overwrite | ||
// the state if the connection is already marked as Disconnected | ||
this.changeState(0 /* Connecting */, 1 /* Connected */); | ||
return [2 /*return*/]; | ||
@@ -328,3 +402,2 @@ case 2: | ||
_a.trys.push([4, 9, , 10]); | ||
this.connectionState = 0 /* Connecting */; | ||
transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); | ||
@@ -342,3 +415,2 @@ if (!(typeof transport === "number")) return [3 /*break*/, 8]; | ||
_a.sent(); | ||
this.changeState(0 /* Connecting */, 1 /* Connected */); | ||
return [2 /*return*/]; | ||
@@ -349,5 +421,9 @@ case 8: return [3 /*break*/, 10]; | ||
this.logger.log(ILogger_1.LogLevel.Error, "Failed to start the transport '" + endpoint.transport + "': " + ex_1); | ||
this.connectionState = 2 /* Disconnected */; | ||
negotiateResponse.connectionId = undefined; | ||
transportExceptions.push(endpoint.transport + " failed: " + ex_1); | ||
if (this.connectionState !== "Connecting " /* Connecting */) { | ||
message = "Failed to select transport before stop() was called."; | ||
this.logger.log(ILogger_1.LogLevel.Debug, message); | ||
return [2 /*return*/, Promise.reject(new Error(message))]; | ||
} | ||
return [3 /*break*/, 10]; | ||
@@ -359,5 +435,5 @@ case 10: | ||
if (transportExceptions.length > 0) { | ||
throw new Error("Unable to connect to the server with any of the available transports. " + transportExceptions.join(" ")); | ||
return [2 /*return*/, Promise.reject(new Error("Unable to connect to the server with any of the available transports. " + transportExceptions.join(" ")))]; | ||
} | ||
throw new Error("None of the transports supported by the client are supported by the server."); | ||
return [2 /*return*/, Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]; | ||
} | ||
@@ -419,13 +495,21 @@ }); | ||
}; | ||
HttpConnection.prototype.changeState = function (from, to) { | ||
if (this.connectionState === from) { | ||
this.connectionState = to; | ||
return true; | ||
} | ||
return false; | ||
}; | ||
HttpConnection.prototype.stopConnection = function (error) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "HttpConnection.stopConnection(" + error + ") called while in state " + this.connectionState + "."); | ||
this.transport = undefined; | ||
// If we have a stopError, it takes precedence over the error from the transport | ||
error = this.stopError || error; | ||
this.stopError = undefined; | ||
if (this.connectionState === "Disconnected" /* Disconnected */) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Call to HttpConnection.stopConnection(" + error + ") was ignored because the connection is already in the disconnected state."); | ||
return; | ||
} | ||
if (this.connectionState === "Connecting " /* Connecting */) { | ||
this.logger.log(ILogger_1.LogLevel.Warning, "Call to HttpConnection.stopConnection(" + error + ") was ignored because the connection hasn't yet left the in the connecting state."); | ||
return; | ||
} | ||
if (this.connectionState === "Disconnecting" /* Disconnecting */) { | ||
// A call to stop() induced this call to stopConnection and needs to be completed. | ||
// Any stop() awaiters will be scheduled to continue after the onclose callback fires. | ||
this.stopPromiseResolver(); | ||
} | ||
if (error) { | ||
@@ -437,5 +521,11 @@ this.logger.log(ILogger_1.LogLevel.Error, "Connection disconnected with error '" + error + "'."); | ||
} | ||
this.connectionState = 2 /* Disconnected */; | ||
if (this.onclose) { | ||
this.onclose(error); | ||
this.connectionState = "Disconnected" /* Disconnected */; | ||
if (this.onclose && this.connectionStarted) { | ||
this.connectionStarted = false; | ||
try { | ||
this.onclose(error); | ||
} | ||
catch (e) { | ||
this.logger.log(ILogger_1.LogLevel.Error, "HttpConnection.onclose(" + error + ") threw error '" + e + "'."); | ||
} | ||
} | ||
@@ -442,0 +532,0 @@ }; |
@@ -51,9 +51,15 @@ "use strict"; | ||
/** The hub connection is disconnected. */ | ||
HubConnectionState[HubConnectionState["Disconnected"] = 0] = "Disconnected"; | ||
HubConnectionState["Disconnected"] = "Disconnected"; | ||
/** The hub connection is connecting. */ | ||
HubConnectionState["Connecting"] = "Connecting"; | ||
/** The hub connection is connected. */ | ||
HubConnectionState[HubConnectionState["Connected"] = 1] = "Connected"; | ||
HubConnectionState["Connected"] = "Connected"; | ||
/** The hub connection is disconnecting. */ | ||
HubConnectionState["Disconnecting"] = "Disconnecting"; | ||
/** The hub connection is reconnecting. */ | ||
HubConnectionState["Reconnecting"] = "Reconnecting"; | ||
})(HubConnectionState = exports.HubConnectionState || (exports.HubConnectionState = {})); | ||
/** Represents a connection to a SignalR Hub. */ | ||
var HubConnection = /** @class */ (function () { | ||
function HubConnection(connection, logger, protocol) { | ||
function HubConnection(connection, logger, protocol, reconnectPolicy) { | ||
var _this = this; | ||
@@ -68,2 +74,3 @@ Utils_1.Arg.isRequired(connection, "connection"); | ||
this.connection = connection; | ||
this.reconnectPolicy = reconnectPolicy; | ||
this.handshakeProtocol = new HandshakeProtocol_1.HandshakeProtocol(); | ||
@@ -75,5 +82,8 @@ this.connection.onreceive = function (data) { return _this.processIncomingData(data); }; | ||
this.closedCallbacks = []; | ||
this.reconnectingCallbacks = []; | ||
this.reconnectedCallbacks = []; | ||
this.invocationId = 0; | ||
this.receivedHandshakeResponse = false; | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
this.cachedPingMessage = this.protocol.writeMessage({ type: IHubProtocol_1.MessageType.Ping }); | ||
@@ -86,4 +96,4 @@ } | ||
// public parameter-less constructor. | ||
HubConnection.create = function (connection, logger, protocol) { | ||
return new HubConnection(connection, logger, protocol); | ||
HubConnection.create = function (connection, logger, protocol, reconnectPolicy) { | ||
return new HubConnection(connection, logger, protocol, reconnectPolicy); | ||
}; | ||
@@ -103,4 +113,39 @@ Object.defineProperty(HubConnection.prototype, "state", { | ||
HubConnection.prototype.start = function () { | ||
this.startPromise = this.startWithStateTransitions(); | ||
return this.startPromise; | ||
}; | ||
HubConnection.prototype.startWithStateTransitions = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var handshakeRequest, handshakePromise; | ||
var e_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (this.connectionState !== HubConnectionState.Disconnected) { | ||
return [2 /*return*/, Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."))]; | ||
} | ||
this.connectionState = HubConnectionState.Connecting; | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Starting HubConnection."); | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.startInternal()]; | ||
case 2: | ||
_a.sent(); | ||
this.connectionState = HubConnectionState.Connected; | ||
this.connectionStarted = true; | ||
this.logger.log(ILogger_1.LogLevel.Debug, "HubConnection connected successfully."); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.logger.log(ILogger_1.LogLevel.Debug, "HubConnection failed to start successfully because of error '" + e_1 + "'."); | ||
return [2 /*return*/, Promise.reject(e_1)]; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HubConnection.prototype.startInternal = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var handshakePromise, handshakeRequest, e_2; | ||
var _this = this; | ||
@@ -110,7 +155,3 @@ return __generator(this, function (_a) { | ||
case 0: | ||
handshakeRequest = { | ||
protocol: this.protocol.name, | ||
version: this.protocol.version, | ||
}; | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Starting HubConnection."); | ||
this.stopDuringStartError = undefined; | ||
this.receivedHandshakeResponse = false; | ||
@@ -124,5 +165,12 @@ handshakePromise = new Promise(function (resolve, reject) { | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 5, , 7]); | ||
handshakeRequest = { | ||
protocol: this.protocol.name, | ||
version: this.protocol.version, | ||
}; | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Sending handshake request."); | ||
return [4 /*yield*/, this.sendMessage(this.handshakeProtocol.writeHandshakeRequest(handshakeRequest))]; | ||
case 2: | ||
case 3: | ||
_a.sent(); | ||
@@ -134,9 +182,23 @@ this.logger.log(ILogger_1.LogLevel.Information, "Using HubProtocol '" + this.protocol.name + "'."); | ||
this.resetKeepAliveInterval(); | ||
// Wait for the handshake to complete before marking connection as connected | ||
return [4 /*yield*/, handshakePromise]; | ||
case 3: | ||
// Wait for the handshake to complete before marking connection as connected | ||
case 4: | ||
_a.sent(); | ||
this.connectionState = HubConnectionState.Connected; | ||
return [2 /*return*/]; | ||
if (this.stopDuringStartError) { | ||
throw this.stopDuringStartError; | ||
} | ||
return [3 /*break*/, 7]; | ||
case 5: | ||
e_2 = _a.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Hub handshake failed with error '" + e_2 + "' during start(). Stopping HubConnection."); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
// HttpConnection.stop() should not complete until after the onclose callback is invoked. | ||
// This will transition the HubConnection to the disconnected state before HttpConnection.stop() completes. | ||
return [4 /*yield*/, this.connection.stop(e_2)]; | ||
case 6: | ||
// HttpConnection.stop() should not complete until after the onclose callback is invoked. | ||
// This will transition the HubConnection to the disconnected state before HttpConnection.stop() completes. | ||
_a.sent(); | ||
throw e_2; | ||
case 7: return [2 /*return*/]; | ||
} | ||
@@ -151,6 +213,57 @@ }); | ||
HubConnection.prototype.stop = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var startPromise, e_3; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
startPromise = this.startPromise; | ||
this.stopPromise = this.stopInternal(); | ||
return [4 /*yield*/, this.stopPromise]; | ||
case 1: | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 4, , 5]); | ||
// Awaiting undefined continues immediately | ||
return [4 /*yield*/, startPromise]; | ||
case 3: | ||
// Awaiting undefined continues immediately | ||
_a.sent(); | ||
return [3 /*break*/, 5]; | ||
case 4: | ||
e_3 = _a.sent(); | ||
return [3 /*break*/, 5]; | ||
case 5: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HubConnection.prototype.stopInternal = function (error) { | ||
if (this.connectionState === HubConnectionState.Disconnected) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Call to HubConnection.stop(" + error + ") ignored because it is already in the disconnected state."); | ||
return Promise.resolve(); | ||
} | ||
if (this.connectionState === HubConnectionState.Disconnecting) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Call to HttpConnection.stop(" + error + ") ignored because the connection is already in the disconnecting state."); | ||
return this.stopPromise; | ||
} | ||
this.connectionState = HubConnectionState.Disconnecting; | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Stopping HubConnection."); | ||
if (this.reconnectDelayHandle) { | ||
// We're in a reconnect delay which means the underlying connection is currently already stopped. | ||
// Just clear the handle to stop the reconnect loop (which no one is waiting on thankfully) and | ||
// fire the onclose callbacks. | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Connection stopped during reconnect delay. Done reconnecting."); | ||
clearTimeout(this.reconnectDelayHandle); | ||
this.reconnectDelayHandle = undefined; | ||
this.completeClose(); | ||
return Promise.resolve(); | ||
} | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
return this.connection.stop(); | ||
this.stopDuringStartError = error || new Error("The connection was stopped before the hub handshake could complete."); | ||
// HttpConnection.stop() should not complete until after either HttpConnection.start() fails | ||
// or the onclose callback is invoked. The onclose callback will transition the HubConnection | ||
// to the disconnected state if need be before HttpConnection.stop() completes. | ||
return this.connection.stop(error); | ||
}; | ||
@@ -340,2 +453,20 @@ /** Invokes a streaming hub method on the server using the specified name and arguments. | ||
}; | ||
/** Registers a handler that will be invoked when the connection starts reconnecting. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection starts reconnecting. Optionally receives a single argument containing the error that caused the connection to start reconnecting (if any). | ||
*/ | ||
HubConnection.prototype.onreconnecting = function (callback) { | ||
if (callback) { | ||
this.reconnectingCallbacks.push(callback); | ||
} | ||
}; | ||
/** Registers a handler that will be invoked when the connection successfully reconnects. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection successfully reconnects. | ||
*/ | ||
HubConnection.prototype.onreconnected = function (callback) { | ||
if (callback) { | ||
this.reconnectedCallbacks.push(callback); | ||
} | ||
}; | ||
HubConnection.prototype.processIncomingData = function (data) { | ||
@@ -360,3 +491,3 @@ this.cleanupTimeout(); | ||
var callback = this.callbacks[message.invocationId]; | ||
if (callback != null) { | ||
if (callback) { | ||
if (message.type === IHubProtocol_1.MessageType.Completion) { | ||
@@ -374,4 +505,3 @@ delete this.callbacks[message.invocationId]; | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(message.error ? new Error("Server returned an error on close: " + message.error) : undefined); | ||
this.stopPromise = this.stopInternal(message.error ? new Error("Server returned an error on close: " + message.error) : undefined); | ||
break; | ||
@@ -397,5 +527,2 @@ default: | ||
var error = new Error(message); | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(error); | ||
this.handshakeRejecter(error); | ||
@@ -407,7 +534,5 @@ throw error; | ||
this.logger.log(ILogger_1.LogLevel.Error, message); | ||
this.handshakeRejecter(message); | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(new Error(message)); | ||
throw new Error(message); | ||
var error = new Error(message); | ||
this.handshakeRejecter(error); | ||
throw error; | ||
} | ||
@@ -456,3 +581,3 @@ else { | ||
// The server hasn't talked to us in a while. It doesn't like us anymore ... :( | ||
// Terminate the connection, but we don't need to wait on the promise. | ||
// Terminate the connection, but we don't need to wait on the promise. This could trigger reconnecting. | ||
// tslint:disable-next-line:no-floating-promises | ||
@@ -465,3 +590,8 @@ this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server.")); | ||
if (methods) { | ||
methods.forEach(function (m) { return m.apply(_this, invocationMessage.arguments); }); | ||
try { | ||
methods.forEach(function (m) { return m.apply(_this, invocationMessage.arguments); }); | ||
} | ||
catch (e) { | ||
this.logger.log(ILogger_1.LogLevel.Error, "A callback for the method " + invocationMessage.target.toLowerCase() + " threw error '" + e + "'."); | ||
} | ||
if (invocationMessage.invocationId) { | ||
@@ -471,5 +601,4 @@ // This is not supported in v1. So we return an error to avoid blocking the server waiting for the response. | ||
this.logger.log(ILogger_1.LogLevel.Error, message); | ||
// We don't need to wait on this Promise. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(new Error(message)); | ||
// We don't want to wait on the stop itself. | ||
this.stopPromise = this.stopInternal(new Error(message)); | ||
} | ||
@@ -482,19 +611,144 @@ } | ||
HubConnection.prototype.connectionClosed = function (error) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "HubConnection.connectionClosed(" + error + ") called while in state " + this.connectionState + "."); | ||
// Triggering this.handshakeRejecter is insufficient because it could already be resolved without the continuation having run yet. | ||
this.stopDuringStartError = this.stopDuringStartError || error || new Error("The underlying connection was closed before the hub handshake could complete."); | ||
// If the handshake is in progress, start will be waiting for the handshake promise, so we complete it. | ||
// If it has already completed, this should just noop. | ||
if (this.handshakeResolver) { | ||
this.handshakeResolver(); | ||
} | ||
this.cancelCallbacksWithError(error || new Error("Invocation canceled due to the underlying connection being closed.")); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
if (this.connectionState === HubConnectionState.Disconnecting) { | ||
this.completeClose(error); | ||
} | ||
else if (this.connectionState === HubConnectionState.Connected && this.reconnectPolicy) { | ||
// tslint:disable-next-line:no-floating-promises | ||
this.reconnect(error); | ||
} | ||
else if (this.connectionState === HubConnectionState.Connected) { | ||
this.completeClose(error); | ||
} | ||
// If none of the above if conditions were true were called the HubConnection must be in either: | ||
// 1. The Connecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail it. | ||
// 2. The Reconnecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail the current reconnect attempt | ||
// and potentially continue the reconnect() loop. | ||
// 3. The Disconnected state in which case we're already done. | ||
}; | ||
HubConnection.prototype.completeClose = function (error) { | ||
var _this = this; | ||
if (this.connectionStarted) { | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
try { | ||
this.closedCallbacks.forEach(function (c) { return c.apply(_this, [error]); }); | ||
} | ||
catch (e) { | ||
this.logger.log(ILogger_1.LogLevel.Error, "An onclose callback called with error '" + error + "' threw error '" + e + "'."); | ||
} | ||
} | ||
}; | ||
HubConnection.prototype.reconnect = function (error) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var reconnectStartTime, previousReconnectAttempts, nextRetryDelay, e_4; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
reconnectStartTime = Date.now(); | ||
previousReconnectAttempts = 0; | ||
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, 0); | ||
if (nextRetryDelay === null) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Connection not reconnecting because the IReconnectPolicy returned null on the first reconnect attempt."); | ||
this.completeClose(error); | ||
return [2 /*return*/]; | ||
} | ||
this.connectionState = HubConnectionState.Reconnecting; | ||
if (error) { | ||
this.logger.log(ILogger_1.LogLevel.Information, "Connection reconnecting because of error '" + error + "'."); | ||
} | ||
else { | ||
this.logger.log(ILogger_1.LogLevel.Information, "Connection reconnecting."); | ||
} | ||
if (this.onreconnecting) { | ||
try { | ||
this.reconnectingCallbacks.forEach(function (c) { return c.apply(_this, [error]); }); | ||
} | ||
catch (e) { | ||
this.logger.log(ILogger_1.LogLevel.Error, "An onreconnecting callback called with error '" + error + "' threw error '" + e + "'."); | ||
} | ||
// Exit early if an onreconnecting callback called connection.stop(). | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Connection left the reconnecting state in onreconnecting callback. Done reconnecting."); | ||
return [2 /*return*/]; | ||
} | ||
} | ||
_a.label = 1; | ||
case 1: | ||
if (!(nextRetryDelay !== null)) return [3 /*break*/, 7]; | ||
this.logger.log(ILogger_1.LogLevel.Information, "Reconnect attempt number " + previousReconnectAttempts + " will start in " + nextRetryDelay + " ms."); | ||
return [4 /*yield*/, new Promise(function (resolve) { | ||
_this.reconnectDelayHandle = setTimeout(resolve, nextRetryDelay); | ||
})]; | ||
case 2: | ||
_a.sent(); | ||
this.reconnectDelayHandle = undefined; | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Connection left the reconnecting state during reconnect delay. Done reconnecting."); | ||
return [2 /*return*/]; | ||
} | ||
_a.label = 3; | ||
case 3: | ||
_a.trys.push([3, 5, , 6]); | ||
return [4 /*yield*/, this.startInternal()]; | ||
case 4: | ||
_a.sent(); | ||
this.connectionState = HubConnectionState.Connected; | ||
this.logger.log(ILogger_1.LogLevel.Information, "HubConnection reconnected successfully."); | ||
if (this.onreconnected) { | ||
try { | ||
this.reconnectedCallbacks.forEach(function (c) { return c.apply(_this, [_this.connection.connectionId]); }); | ||
} | ||
catch (e) { | ||
this.logger.log(ILogger_1.LogLevel.Error, "An onreconnected callback called with connectionId '" + this.connection.connectionId + "; threw error '" + e + "'."); | ||
} | ||
} | ||
return [2 /*return*/]; | ||
case 5: | ||
e_4 = _a.sent(); | ||
this.logger.log(ILogger_1.LogLevel.Information, "Reconnect attempt failed because of error '" + e_4 + "'."); | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(ILogger_1.LogLevel.Debug, "Connection left the reconnecting state during reconnect attempt. Done reconnecting."); | ||
return [2 /*return*/]; | ||
} | ||
return [3 /*break*/, 6]; | ||
case 6: | ||
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, Date.now() - reconnectStartTime); | ||
return [3 /*break*/, 1]; | ||
case 7: | ||
this.logger.log(ILogger_1.LogLevel.Information, "Reconnect retries have been exhausted after " + (Date.now() - reconnectStartTime) + " ms and " + previousReconnectAttempts + " failed attempts. Connection disconnecting."); | ||
this.completeClose(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HubConnection.prototype.getNextRetryDelay = function (previousRetryCount, elapsedMilliseconds) { | ||
try { | ||
return this.reconnectPolicy.nextRetryDelayInMilliseconds(previousRetryCount, elapsedMilliseconds); | ||
} | ||
catch (e) { | ||
this.logger.log(ILogger_1.LogLevel.Error, "IReconnectPolicy.nextRetryDelayInMilliseconds(" + previousRetryCount + ", " + elapsedMilliseconds + ") threw error '" + e + "'."); | ||
return null; | ||
} | ||
}; | ||
HubConnection.prototype.cancelCallbacksWithError = function (error) { | ||
var callbacks = this.callbacks; | ||
this.callbacks = {}; | ||
this.connectionState = HubConnectionState.Disconnected; | ||
// if handshake is in progress start will be waiting for the handshake promise, so we complete it | ||
// if it has already completed this should just noop | ||
if (this.handshakeRejecter) { | ||
this.handshakeRejecter(error); | ||
} | ||
Object.keys(callbacks) | ||
.forEach(function (key) { | ||
var callback = callbacks[key]; | ||
callback(null, error ? error : new Error("Invocation canceled due to connection being closed.")); | ||
callback(null, error); | ||
}); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
this.closedCallbacks.forEach(function (c) { return c.apply(_this, [error]); }); | ||
}; | ||
@@ -501,0 +755,0 @@ HubConnection.prototype.cleanupPingTimer = function () { |
"use strict"; | ||
// 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. | ||
var __assign = (this && this.__assign) || Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var DefaultReconnectPolicy_1 = require("./DefaultReconnectPolicy"); | ||
var HttpConnection_1 = require("./HttpConnection"); | ||
@@ -30,8 +39,6 @@ var HubConnection_1 = require("./HubConnection"); | ||
if (typeof transportTypeOrOptions === "object") { | ||
this.httpConnectionOptions = transportTypeOrOptions; | ||
this.httpConnectionOptions = __assign({}, this.httpConnectionOptions, transportTypeOrOptions); | ||
} | ||
else { | ||
this.httpConnectionOptions = { | ||
transport: transportTypeOrOptions, | ||
}; | ||
this.httpConnectionOptions = __assign({}, this.httpConnectionOptions, { transport: transportTypeOrOptions }); | ||
} | ||
@@ -49,2 +56,17 @@ return this; | ||
}; | ||
HubConnectionBuilder.prototype.withAutomaticReconnect = function (retryDelaysOrReconnectPolicy) { | ||
if (this.reconnectPolicy) { | ||
throw new Error("A reconnectPolicy has already been set."); | ||
} | ||
if (!retryDelaysOrReconnectPolicy) { | ||
this.reconnectPolicy = new DefaultReconnectPolicy_1.DefaultReconnectPolicy(); | ||
} | ||
else if (Array.isArray(retryDelaysOrReconnectPolicy)) { | ||
this.reconnectPolicy = new DefaultReconnectPolicy_1.DefaultReconnectPolicy(retryDelaysOrReconnectPolicy); | ||
} | ||
else { | ||
this.reconnectPolicy = retryDelaysOrReconnectPolicy; | ||
} | ||
return this; | ||
}; | ||
/** Creates a {@link @aspnet/signalr.HubConnection} from the configuration options specified in this builder. | ||
@@ -68,3 +90,3 @@ * | ||
var connection = new HttpConnection_1.HttpConnection(this.url, httpConnectionOptions); | ||
return HubConnection_1.HubConnection.create(connection, this.logger || Loggers_1.NullLogger.instance, this.protocol || new JsonHubProtocol_1.JsonHubProtocol()); | ||
return HubConnection_1.HubConnection.create(connection, this.logger || Loggers_1.NullLogger.instance, this.protocol || new JsonHubProtocol_1.JsonHubProtocol(), this.reconnectPolicy); | ||
}; | ||
@@ -71,0 +93,0 @@ return HubConnectionBuilder; |
@@ -7,3 +7,3 @@ "use strict"; | ||
/** The version of the SignalR client. */ | ||
exports.VERSION = "3.0.0-preview3-19153-02"; | ||
exports.VERSION = "3.0.0-preview4-19216-03"; | ||
var Errors_1 = require("./Errors"); | ||
@@ -10,0 +10,0 @@ exports.AbortError = Errors_1.AbortError; |
@@ -23,3 +23,2 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var Request = require("request"); | ||
var Errors_1 = require("./Errors"); | ||
@@ -29,2 +28,9 @@ var HttpClient_1 = require("./HttpClient"); | ||
var Utils_1 = require("./Utils"); | ||
var requestModule; | ||
if (typeof XMLHttpRequest === "undefined") { | ||
// In order to ignore the dynamic require in webpack builds we need to do this magic | ||
// @ts-ignore: TS doesn't know about these names | ||
var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; | ||
requestModule = requireFunc("request"); | ||
} | ||
var NodeHttpClient = /** @class */ (function (_super) { | ||
@@ -34,5 +40,8 @@ __extends(NodeHttpClient, _super); | ||
var _this = _super.call(this) || this; | ||
if (typeof requestModule === "undefined") { | ||
throw new Error("The 'request' module could not be loaded."); | ||
} | ||
_this.logger = logger; | ||
_this.cookieJar = Request.jar(); | ||
_this.request = Request.defaults({ jar: _this.cookieJar }); | ||
_this.cookieJar = requestModule.jar(); | ||
_this.request = requestModule.defaults({ jar: _this.cookieJar }); | ||
return _this; | ||
@@ -39,0 +48,0 @@ } |
@@ -15,10 +15,4 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
import { HttpClient } from "./HttpClient"; | ||
import { NodeHttpClient } from "./NodeHttpClient"; | ||
import { XhrHttpClient } from "./XhrHttpClient"; | ||
var nodeHttpClientModule; | ||
if (typeof XMLHttpRequest === "undefined") { | ||
// In order to ignore the dynamic require in webpack builds we need to do this magic | ||
// @ts-ignore: TS doesn't know about these names | ||
var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; | ||
nodeHttpClientModule = requireFunc("./NodeHttpClient"); | ||
} | ||
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */ | ||
@@ -33,7 +27,4 @@ var DefaultHttpClient = /** @class */ (function (_super) { | ||
} | ||
else if (typeof nodeHttpClientModule !== "undefined") { | ||
_this.httpClient = new nodeHttpClientModule.NodeHttpClient(logger); | ||
} | ||
else { | ||
throw new Error("No HttpClient could be created."); | ||
_this.httpClient = new NodeHttpClient(logger); | ||
} | ||
@@ -40,0 +31,0 @@ return _this; |
@@ -20,3 +20,4 @@ import { IConnection } from "./IConnection"; | ||
private connectionState; | ||
private baseUrl; | ||
private connectionStarted; | ||
private readonly baseUrl; | ||
private readonly httpClient; | ||
@@ -26,6 +27,9 @@ private readonly logger; | ||
private transport?; | ||
private startPromise?; | ||
private startInternalPromise?; | ||
private stopPromise?; | ||
private stopPromiseResolver; | ||
private stopError?; | ||
private accessTokenFactory?; | ||
readonly features: any; | ||
connectionId?: string; | ||
onreceive: ((data: string | ArrayBuffer) => void) | null; | ||
@@ -38,2 +42,3 @@ onclose: ((e?: Error) => void) | null; | ||
stop(error?: Error): Promise<void>; | ||
private stopInternal; | ||
private startInternal; | ||
@@ -46,3 +51,2 @@ private getNegotiationResponse; | ||
private isITransport; | ||
private changeState; | ||
private stopConnection; | ||
@@ -49,0 +53,0 @@ private resolveUrl; |
@@ -82,3 +82,4 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
this.httpClient = options.httpClient || new DefaultHttpClient(this.logger); | ||
this.connectionState = 2 /* Disconnected */; | ||
this.connectionState = "Disconnected" /* Disconnected */; | ||
this.connectionStarted = false; | ||
this.options = options; | ||
@@ -89,15 +90,44 @@ this.onreceive = null; | ||
HttpConnection.prototype.start = function (transferFormat) { | ||
transferFormat = transferFormat || TransferFormat.Binary; | ||
Arg.isIn(transferFormat, TransferFormat, "transferFormat"); | ||
this.logger.log(LogLevel.Debug, "Starting connection with transfer format '" + TransferFormat[transferFormat] + "'."); | ||
if (this.connectionState !== 2 /* Disconnected */) { | ||
return Promise.reject(new Error("Cannot start a connection that is not in the 'Disconnected' state.")); | ||
} | ||
this.connectionState = 0 /* Connecting */; | ||
this.startPromise = this.startInternal(transferFormat); | ||
return this.startPromise; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var message, message; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
transferFormat = transferFormat || TransferFormat.Binary; | ||
Arg.isIn(transferFormat, TransferFormat, "transferFormat"); | ||
this.logger.log(LogLevel.Debug, "Starting connection with transfer format '" + TransferFormat[transferFormat] + "'."); | ||
if (this.connectionState !== "Disconnected" /* Disconnected */) { | ||
return [2 /*return*/, Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."))]; | ||
} | ||
this.connectionState = "Connecting " /* Connecting */; | ||
this.startInternalPromise = this.startInternal(transferFormat); | ||
return [4 /*yield*/, this.startInternalPromise]; | ||
case 1: | ||
_a.sent(); | ||
if (!(this.connectionState === "Disconnecting" /* Disconnecting */)) return [3 /*break*/, 3]; | ||
message = "Failed to start the HttpConnection before stop() was called."; | ||
this.logger.log(LogLevel.Error, message); | ||
// We cannot await stopPromise inside startInternal since stopInternal awaits the startInternalPromise. | ||
return [4 /*yield*/, this.stopPromise]; | ||
case 2: | ||
// We cannot await stopPromise inside startInternal since stopInternal awaits the startInternalPromise. | ||
_a.sent(); | ||
return [2 /*return*/, Promise.reject(new Error(message))]; | ||
case 3: | ||
if (this.connectionState !== "Connected" /* Connected */) { | ||
message = "HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!"; | ||
this.logger.log(LogLevel.Error, message); | ||
return [2 /*return*/, Promise.reject(new Error(message))]; | ||
} | ||
_a.label = 4; | ||
case 4: | ||
this.connectionStarted = true; | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HttpConnection.prototype.send = function (data) { | ||
if (this.connectionState !== 1 /* Connected */) { | ||
throw new Error("Cannot send data if the connection is not in the 'Connected' State."); | ||
if (this.connectionState !== "Connected" /* Connected */) { | ||
return Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")); | ||
} | ||
@@ -109,7 +139,38 @@ // Transport will not be null if state is connected | ||
return __awaiter(this, void 0, void 0, function () { | ||
var e_1; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
this.connectionState = 2 /* Disconnected */; | ||
if (this.connectionState === "Disconnected" /* Disconnected */) { | ||
this.logger.log(LogLevel.Debug, "Call to HttpConnection.stop(" + error + ") ignored because the connection is already in the disconnected state."); | ||
return [2 /*return*/, Promise.resolve()]; | ||
} | ||
if (this.connectionState === "Disconnecting" /* Disconnecting */) { | ||
this.logger.log(LogLevel.Debug, "Call to HttpConnection.stop(" + error + ") ignored because the connection is already in the disconnecting state."); | ||
return [2 /*return*/, this.stopPromise]; | ||
} | ||
this.connectionState = "Disconnecting" /* Disconnecting */; | ||
this.stopPromise = new Promise(function (resolve) { | ||
// Don't complete stop() until stopConnection() completes. | ||
_this.stopPromiseResolver = resolve; | ||
}); | ||
// stopInternal should never throw so just observe it. | ||
return [4 /*yield*/, this.stopInternal(error)]; | ||
case 1: | ||
// stopInternal should never throw so just observe it. | ||
_a.sent(); | ||
return [4 /*yield*/, this.stopPromise]; | ||
case 2: | ||
_a.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HttpConnection.prototype.stopInternal = function (error) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var e_1, e_2; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
// Set error as soon as possible otherwise there is a race between | ||
@@ -122,3 +183,3 @@ // the transport closing and providing an error and the error from a close message | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.startPromise]; | ||
return [4 /*yield*/, this.startInternalPromise]; | ||
case 2: | ||
@@ -131,9 +192,23 @@ _a.sent(); | ||
case 4: | ||
if (!this.transport) return [3 /*break*/, 6]; | ||
if (!this.transport) return [3 /*break*/, 9]; | ||
_a.label = 5; | ||
case 5: | ||
_a.trys.push([5, 7, , 8]); | ||
return [4 /*yield*/, this.transport.stop()]; | ||
case 5: | ||
case 6: | ||
_a.sent(); | ||
return [3 /*break*/, 8]; | ||
case 7: | ||
e_2 = _a.sent(); | ||
this.logger.log(LogLevel.Error, "HttpConnection.transport.stop() threw error '" + e_2 + "'."); | ||
this.stopConnection(); | ||
return [3 /*break*/, 8]; | ||
case 8: | ||
this.transport = undefined; | ||
_a.label = 6; | ||
case 6: return [2 /*return*/]; | ||
return [3 /*break*/, 10]; | ||
case 9: | ||
this.logger.log(LogLevel.Debug, "HttpConnection.transport is undefined in HttpConnection.stop() because start() failed."); | ||
this.stopConnection(); | ||
_a.label = 10; | ||
case 10: return [2 /*return*/]; | ||
} | ||
@@ -145,3 +220,3 @@ }); | ||
return __awaiter(this, void 0, void 0, function () { | ||
var url, negotiateResponse, redirects, _loop_1, this_1, state_1, e_2; | ||
var url, negotiateResponse, redirects, _loop_1, this_1, e_3; | ||
var _this = this; | ||
@@ -168,3 +243,3 @@ return __generator(this, function (_a) { | ||
return [3 /*break*/, 4]; | ||
case 3: throw Error("Negotiation can only be skipped when using the WebSocket transport directly."); | ||
case 3: throw new Error("Negotiation can only be skipped when using the WebSocket transport directly."); | ||
case 4: return [3 /*break*/, 11]; | ||
@@ -182,10 +257,10 @@ case 5: | ||
// the user tries to stop the connection when it is being started | ||
if (this_1.connectionState === 2 /* Disconnected */) { | ||
return [2 /*return*/, { value: void 0 }]; | ||
if (this_1.connectionState === "Disconnecting" /* Disconnecting */ || this_1.connectionState === "Disconnected" /* Disconnected */) { | ||
throw new Error("The connection was stopped during negotiation."); | ||
} | ||
if (negotiateResponse.error) { | ||
throw Error(negotiateResponse.error); | ||
throw new Error(negotiateResponse.error); | ||
} | ||
if (negotiateResponse.ProtocolVersion) { | ||
throw Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); | ||
throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); | ||
} | ||
@@ -208,5 +283,3 @@ if (negotiateResponse.url) { | ||
case 7: | ||
state_1 = _a.sent(); | ||
if (typeof state_1 === "object") | ||
return [2 /*return*/, state_1.value]; | ||
_a.sent(); | ||
_a.label = 8; | ||
@@ -218,4 +291,5 @@ case 8: | ||
if (redirects === MAX_REDIRECTS && negotiateResponse.url) { | ||
throw Error("Negotiate redirection limit exceeded."); | ||
throw new Error("Negotiate redirection limit exceeded."); | ||
} | ||
this.connectionId = negotiateResponse.connectionId; | ||
return [4 /*yield*/, this.createTransport(url, this.options.transport, negotiateResponse, transferFormat)]; | ||
@@ -231,12 +305,15 @@ case 10: | ||
this.transport.onclose = function (e) { return _this.stopConnection(e); }; | ||
// only change the state if we were connecting to not overwrite | ||
// the state if the connection is already marked as Disconnected | ||
this.changeState(0 /* Connecting */, 1 /* Connected */); | ||
if (this.connectionState === "Connecting " /* Connecting */) { | ||
// Ensure the connection transitions to the connected state prior to completing this.startInternalPromise. | ||
// start() will handle the case when stop was called and startInternal exits still in the disconnecting state. | ||
this.logger.log(LogLevel.Debug, "The HttpConnection connected successfully."); | ||
this.connectionState = "Connected" /* Connected */; | ||
} | ||
return [3 /*break*/, 13]; | ||
case 12: | ||
e_2 = _a.sent(); | ||
this.logger.log(LogLevel.Error, "Failed to start the connection: " + e_2); | ||
this.connectionState = 2 /* Disconnected */; | ||
e_3 = _a.sent(); | ||
this.logger.log(LogLevel.Error, "Failed to start the connection: " + e_3); | ||
this.connectionState = "Disconnected" /* Disconnected */; | ||
this.transport = undefined; | ||
throw e_2; | ||
return [2 /*return*/, Promise.reject(e_3)]; | ||
case 13: return [2 /*return*/]; | ||
@@ -249,3 +326,3 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a, headers, token, negotiateUrl, response, e_3; | ||
var _a, headers, token, negotiateUrl, response, e_4; | ||
return __generator(this, function (_b) { | ||
@@ -277,9 +354,9 @@ switch (_b.label) { | ||
if (response.statusCode !== 200) { | ||
throw Error("Unexpected status code returned from negotiate " + response.statusCode); | ||
return [2 /*return*/, Promise.reject(new Error("Unexpected status code returned from negotiate " + response.statusCode))]; | ||
} | ||
return [2 /*return*/, JSON.parse(response.content)]; | ||
case 5: | ||
e_3 = _b.sent(); | ||
this.logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e_3); | ||
throw e_3; | ||
e_4 = _b.sent(); | ||
this.logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e_4); | ||
return [2 /*return*/, Promise.reject(e_4)]; | ||
case 6: return [2 /*return*/]; | ||
@@ -298,3 +375,3 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var connectUrl, transportExceptions, transports, _i, transports_1, endpoint, transport, ex_1; | ||
var connectUrl, transportExceptions, transports, _i, transports_1, endpoint, transport, ex_1, message; | ||
return __generator(this, function (_a) { | ||
@@ -310,5 +387,2 @@ switch (_a.label) { | ||
_a.sent(); | ||
// only change the state if we were connecting to not overwrite | ||
// the state if the connection is already marked as Disconnected | ||
this.changeState(0 /* Connecting */, 1 /* Connected */); | ||
return [2 /*return*/]; | ||
@@ -326,3 +400,2 @@ case 2: | ||
_a.trys.push([4, 9, , 10]); | ||
this.connectionState = 0 /* Connecting */; | ||
transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); | ||
@@ -340,3 +413,2 @@ if (!(typeof transport === "number")) return [3 /*break*/, 8]; | ||
_a.sent(); | ||
this.changeState(0 /* Connecting */, 1 /* Connected */); | ||
return [2 /*return*/]; | ||
@@ -347,5 +419,9 @@ case 8: return [3 /*break*/, 10]; | ||
this.logger.log(LogLevel.Error, "Failed to start the transport '" + endpoint.transport + "': " + ex_1); | ||
this.connectionState = 2 /* Disconnected */; | ||
negotiateResponse.connectionId = undefined; | ||
transportExceptions.push(endpoint.transport + " failed: " + ex_1); | ||
if (this.connectionState !== "Connecting " /* Connecting */) { | ||
message = "Failed to select transport before stop() was called."; | ||
this.logger.log(LogLevel.Debug, message); | ||
return [2 /*return*/, Promise.reject(new Error(message))]; | ||
} | ||
return [3 /*break*/, 10]; | ||
@@ -357,5 +433,5 @@ case 10: | ||
if (transportExceptions.length > 0) { | ||
throw new Error("Unable to connect to the server with any of the available transports. " + transportExceptions.join(" ")); | ||
return [2 /*return*/, Promise.reject(new Error("Unable to connect to the server with any of the available transports. " + transportExceptions.join(" ")))]; | ||
} | ||
throw new Error("None of the transports supported by the client are supported by the server."); | ||
return [2 /*return*/, Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]; | ||
} | ||
@@ -417,13 +493,21 @@ }); | ||
}; | ||
HttpConnection.prototype.changeState = function (from, to) { | ||
if (this.connectionState === from) { | ||
this.connectionState = to; | ||
return true; | ||
} | ||
return false; | ||
}; | ||
HttpConnection.prototype.stopConnection = function (error) { | ||
this.logger.log(LogLevel.Debug, "HttpConnection.stopConnection(" + error + ") called while in state " + this.connectionState + "."); | ||
this.transport = undefined; | ||
// If we have a stopError, it takes precedence over the error from the transport | ||
error = this.stopError || error; | ||
this.stopError = undefined; | ||
if (this.connectionState === "Disconnected" /* Disconnected */) { | ||
this.logger.log(LogLevel.Debug, "Call to HttpConnection.stopConnection(" + error + ") was ignored because the connection is already in the disconnected state."); | ||
return; | ||
} | ||
if (this.connectionState === "Connecting " /* Connecting */) { | ||
this.logger.log(LogLevel.Warning, "Call to HttpConnection.stopConnection(" + error + ") was ignored because the connection hasn't yet left the in the connecting state."); | ||
return; | ||
} | ||
if (this.connectionState === "Disconnecting" /* Disconnecting */) { | ||
// A call to stop() induced this call to stopConnection and needs to be completed. | ||
// Any stop() awaiters will be scheduled to continue after the onclose callback fires. | ||
this.stopPromiseResolver(); | ||
} | ||
if (error) { | ||
@@ -435,5 +519,11 @@ this.logger.log(LogLevel.Error, "Connection disconnected with error '" + error + "'."); | ||
} | ||
this.connectionState = 2 /* Disconnected */; | ||
if (this.onclose) { | ||
this.onclose(error); | ||
this.connectionState = "Disconnected" /* Disconnected */; | ||
if (this.onclose && this.connectionStarted) { | ||
this.connectionStarted = false; | ||
try { | ||
this.onclose(error); | ||
} | ||
catch (e) { | ||
this.logger.log(LogLevel.Error, "HttpConnection.onclose(" + error + ") threw error '" + e + "'."); | ||
} | ||
} | ||
@@ -440,0 +530,0 @@ }; |
@@ -5,5 +5,11 @@ import { IStreamResult } from "./Stream"; | ||
/** The hub connection is disconnected. */ | ||
Disconnected = 0, | ||
Disconnected = "Disconnected", | ||
/** The hub connection is connecting. */ | ||
Connecting = "Connecting", | ||
/** The hub connection is connected. */ | ||
Connected = 1 | ||
Connected = "Connected", | ||
/** The hub connection is disconnecting. */ | ||
Disconnecting = "Disconnecting", | ||
/** The hub connection is reconnecting. */ | ||
Reconnecting = "Reconnecting" | ||
} | ||
@@ -15,2 +21,3 @@ /** Represents a connection to a SignalR Hub. */ | ||
private readonly logger; | ||
private readonly reconnectPolicy?; | ||
private protocol; | ||
@@ -22,6 +29,13 @@ private handshakeProtocol; | ||
private closedCallbacks; | ||
private reconnectingCallbacks; | ||
private reconnectedCallbacks; | ||
private receivedHandshakeResponse; | ||
private handshakeResolver; | ||
private handshakeRejecter; | ||
private stopDuringStartError?; | ||
private connectionState; | ||
private connectionStarted; | ||
private startPromise?; | ||
private stopPromise?; | ||
private reconnectDelayHandle?; | ||
private timeoutHandle?; | ||
@@ -49,2 +63,4 @@ private pingServerHandle?; | ||
start(): Promise<void>; | ||
private startWithStateTransitions; | ||
private startInternal; | ||
/** Stops the connection. | ||
@@ -55,2 +71,3 @@ * | ||
stop(): Promise<void>; | ||
private stopInternal; | ||
/** Invokes a streaming hub method on the server using the specified name and arguments. | ||
@@ -117,2 +134,12 @@ * | ||
onclose(callback: (error?: Error) => void): void; | ||
/** Registers a handler that will be invoked when the connection starts reconnecting. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection starts reconnecting. Optionally receives a single argument containing the error that caused the connection to start reconnecting (if any). | ||
*/ | ||
onreconnecting(callback: (error?: Error) => void): void; | ||
/** Registers a handler that will be invoked when the connection successfully reconnects. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection successfully reconnects. | ||
*/ | ||
onreconnected(callback: (connectionId?: string) => void): void; | ||
private processIncomingData; | ||
@@ -125,2 +152,6 @@ private processHandshakeResponse; | ||
private connectionClosed; | ||
private completeClose; | ||
private reconnect; | ||
private getNextRetryDelay; | ||
private cancelCallbacksWithError; | ||
private cleanupPingTimer; | ||
@@ -127,0 +158,0 @@ private cleanupTimeout; |
@@ -49,9 +49,15 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
/** The hub connection is disconnected. */ | ||
HubConnectionState[HubConnectionState["Disconnected"] = 0] = "Disconnected"; | ||
HubConnectionState["Disconnected"] = "Disconnected"; | ||
/** The hub connection is connecting. */ | ||
HubConnectionState["Connecting"] = "Connecting"; | ||
/** The hub connection is connected. */ | ||
HubConnectionState[HubConnectionState["Connected"] = 1] = "Connected"; | ||
HubConnectionState["Connected"] = "Connected"; | ||
/** The hub connection is disconnecting. */ | ||
HubConnectionState["Disconnecting"] = "Disconnecting"; | ||
/** The hub connection is reconnecting. */ | ||
HubConnectionState["Reconnecting"] = "Reconnecting"; | ||
})(HubConnectionState || (HubConnectionState = {})); | ||
/** Represents a connection to a SignalR Hub. */ | ||
var HubConnection = /** @class */ (function () { | ||
function HubConnection(connection, logger, protocol) { | ||
function HubConnection(connection, logger, protocol, reconnectPolicy) { | ||
var _this = this; | ||
@@ -66,2 +72,3 @@ Arg.isRequired(connection, "connection"); | ||
this.connection = connection; | ||
this.reconnectPolicy = reconnectPolicy; | ||
this.handshakeProtocol = new HandshakeProtocol(); | ||
@@ -73,5 +80,8 @@ this.connection.onreceive = function (data) { return _this.processIncomingData(data); }; | ||
this.closedCallbacks = []; | ||
this.reconnectingCallbacks = []; | ||
this.reconnectedCallbacks = []; | ||
this.invocationId = 0; | ||
this.receivedHandshakeResponse = false; | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
this.cachedPingMessage = this.protocol.writeMessage({ type: MessageType.Ping }); | ||
@@ -84,4 +94,4 @@ } | ||
// public parameter-less constructor. | ||
HubConnection.create = function (connection, logger, protocol) { | ||
return new HubConnection(connection, logger, protocol); | ||
HubConnection.create = function (connection, logger, protocol, reconnectPolicy) { | ||
return new HubConnection(connection, logger, protocol, reconnectPolicy); | ||
}; | ||
@@ -101,4 +111,39 @@ Object.defineProperty(HubConnection.prototype, "state", { | ||
HubConnection.prototype.start = function () { | ||
this.startPromise = this.startWithStateTransitions(); | ||
return this.startPromise; | ||
}; | ||
HubConnection.prototype.startWithStateTransitions = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var handshakeRequest, handshakePromise; | ||
var e_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (this.connectionState !== HubConnectionState.Disconnected) { | ||
return [2 /*return*/, Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."))]; | ||
} | ||
this.connectionState = HubConnectionState.Connecting; | ||
this.logger.log(LogLevel.Debug, "Starting HubConnection."); | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, this.startInternal()]; | ||
case 2: | ||
_a.sent(); | ||
this.connectionState = HubConnectionState.Connected; | ||
this.connectionStarted = true; | ||
this.logger.log(LogLevel.Debug, "HubConnection connected successfully."); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.logger.log(LogLevel.Debug, "HubConnection failed to start successfully because of error '" + e_1 + "'."); | ||
return [2 /*return*/, Promise.reject(e_1)]; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HubConnection.prototype.startInternal = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var handshakePromise, handshakeRequest, e_2; | ||
var _this = this; | ||
@@ -108,7 +153,3 @@ return __generator(this, function (_a) { | ||
case 0: | ||
handshakeRequest = { | ||
protocol: this.protocol.name, | ||
version: this.protocol.version, | ||
}; | ||
this.logger.log(LogLevel.Debug, "Starting HubConnection."); | ||
this.stopDuringStartError = undefined; | ||
this.receivedHandshakeResponse = false; | ||
@@ -122,5 +163,12 @@ handshakePromise = new Promise(function (resolve, reject) { | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 5, , 7]); | ||
handshakeRequest = { | ||
protocol: this.protocol.name, | ||
version: this.protocol.version, | ||
}; | ||
this.logger.log(LogLevel.Debug, "Sending handshake request."); | ||
return [4 /*yield*/, this.sendMessage(this.handshakeProtocol.writeHandshakeRequest(handshakeRequest))]; | ||
case 2: | ||
case 3: | ||
_a.sent(); | ||
@@ -132,9 +180,23 @@ this.logger.log(LogLevel.Information, "Using HubProtocol '" + this.protocol.name + "'."); | ||
this.resetKeepAliveInterval(); | ||
// Wait for the handshake to complete before marking connection as connected | ||
return [4 /*yield*/, handshakePromise]; | ||
case 3: | ||
// Wait for the handshake to complete before marking connection as connected | ||
case 4: | ||
_a.sent(); | ||
this.connectionState = HubConnectionState.Connected; | ||
return [2 /*return*/]; | ||
if (this.stopDuringStartError) { | ||
throw this.stopDuringStartError; | ||
} | ||
return [3 /*break*/, 7]; | ||
case 5: | ||
e_2 = _a.sent(); | ||
this.logger.log(LogLevel.Debug, "Hub handshake failed with error '" + e_2 + "' during start(). Stopping HubConnection."); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
// HttpConnection.stop() should not complete until after the onclose callback is invoked. | ||
// This will transition the HubConnection to the disconnected state before HttpConnection.stop() completes. | ||
return [4 /*yield*/, this.connection.stop(e_2)]; | ||
case 6: | ||
// HttpConnection.stop() should not complete until after the onclose callback is invoked. | ||
// This will transition the HubConnection to the disconnected state before HttpConnection.stop() completes. | ||
_a.sent(); | ||
throw e_2; | ||
case 7: return [2 /*return*/]; | ||
} | ||
@@ -149,6 +211,57 @@ }); | ||
HubConnection.prototype.stop = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var startPromise, e_3; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
startPromise = this.startPromise; | ||
this.stopPromise = this.stopInternal(); | ||
return [4 /*yield*/, this.stopPromise]; | ||
case 1: | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
_a.trys.push([2, 4, , 5]); | ||
// Awaiting undefined continues immediately | ||
return [4 /*yield*/, startPromise]; | ||
case 3: | ||
// Awaiting undefined continues immediately | ||
_a.sent(); | ||
return [3 /*break*/, 5]; | ||
case 4: | ||
e_3 = _a.sent(); | ||
return [3 /*break*/, 5]; | ||
case 5: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HubConnection.prototype.stopInternal = function (error) { | ||
if (this.connectionState === HubConnectionState.Disconnected) { | ||
this.logger.log(LogLevel.Debug, "Call to HubConnection.stop(" + error + ") ignored because it is already in the disconnected state."); | ||
return Promise.resolve(); | ||
} | ||
if (this.connectionState === HubConnectionState.Disconnecting) { | ||
this.logger.log(LogLevel.Debug, "Call to HttpConnection.stop(" + error + ") ignored because the connection is already in the disconnecting state."); | ||
return this.stopPromise; | ||
} | ||
this.connectionState = HubConnectionState.Disconnecting; | ||
this.logger.log(LogLevel.Debug, "Stopping HubConnection."); | ||
if (this.reconnectDelayHandle) { | ||
// We're in a reconnect delay which means the underlying connection is currently already stopped. | ||
// Just clear the handle to stop the reconnect loop (which no one is waiting on thankfully) and | ||
// fire the onclose callbacks. | ||
this.logger.log(LogLevel.Debug, "Connection stopped during reconnect delay. Done reconnecting."); | ||
clearTimeout(this.reconnectDelayHandle); | ||
this.reconnectDelayHandle = undefined; | ||
this.completeClose(); | ||
return Promise.resolve(); | ||
} | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
return this.connection.stop(); | ||
this.stopDuringStartError = error || new Error("The connection was stopped before the hub handshake could complete."); | ||
// HttpConnection.stop() should not complete until after either HttpConnection.start() fails | ||
// or the onclose callback is invoked. The onclose callback will transition the HubConnection | ||
// to the disconnected state if need be before HttpConnection.stop() completes. | ||
return this.connection.stop(error); | ||
}; | ||
@@ -338,2 +451,20 @@ /** Invokes a streaming hub method on the server using the specified name and arguments. | ||
}; | ||
/** Registers a handler that will be invoked when the connection starts reconnecting. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection starts reconnecting. Optionally receives a single argument containing the error that caused the connection to start reconnecting (if any). | ||
*/ | ||
HubConnection.prototype.onreconnecting = function (callback) { | ||
if (callback) { | ||
this.reconnectingCallbacks.push(callback); | ||
} | ||
}; | ||
/** Registers a handler that will be invoked when the connection successfully reconnects. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection successfully reconnects. | ||
*/ | ||
HubConnection.prototype.onreconnected = function (callback) { | ||
if (callback) { | ||
this.reconnectedCallbacks.push(callback); | ||
} | ||
}; | ||
HubConnection.prototype.processIncomingData = function (data) { | ||
@@ -358,3 +489,3 @@ this.cleanupTimeout(); | ||
var callback = this.callbacks[message.invocationId]; | ||
if (callback != null) { | ||
if (callback) { | ||
if (message.type === MessageType.Completion) { | ||
@@ -372,4 +503,3 @@ delete this.callbacks[message.invocationId]; | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(message.error ? new Error("Server returned an error on close: " + message.error) : undefined); | ||
this.stopPromise = this.stopInternal(message.error ? new Error("Server returned an error on close: " + message.error) : undefined); | ||
break; | ||
@@ -395,5 +525,2 @@ default: | ||
var error = new Error(message); | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(error); | ||
this.handshakeRejecter(error); | ||
@@ -405,7 +532,5 @@ throw error; | ||
this.logger.log(LogLevel.Error, message); | ||
this.handshakeRejecter(message); | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(new Error(message)); | ||
throw new Error(message); | ||
var error = new Error(message); | ||
this.handshakeRejecter(error); | ||
throw error; | ||
} | ||
@@ -454,3 +579,3 @@ else { | ||
// The server hasn't talked to us in a while. It doesn't like us anymore ... :( | ||
// Terminate the connection, but we don't need to wait on the promise. | ||
// Terminate the connection, but we don't need to wait on the promise. This could trigger reconnecting. | ||
// tslint:disable-next-line:no-floating-promises | ||
@@ -463,3 +588,8 @@ this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server.")); | ||
if (methods) { | ||
methods.forEach(function (m) { return m.apply(_this, invocationMessage.arguments); }); | ||
try { | ||
methods.forEach(function (m) { return m.apply(_this, invocationMessage.arguments); }); | ||
} | ||
catch (e) { | ||
this.logger.log(LogLevel.Error, "A callback for the method " + invocationMessage.target.toLowerCase() + " threw error '" + e + "'."); | ||
} | ||
if (invocationMessage.invocationId) { | ||
@@ -469,5 +599,4 @@ // This is not supported in v1. So we return an error to avoid blocking the server waiting for the response. | ||
this.logger.log(LogLevel.Error, message); | ||
// We don't need to wait on this Promise. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(new Error(message)); | ||
// We don't want to wait on the stop itself. | ||
this.stopPromise = this.stopInternal(new Error(message)); | ||
} | ||
@@ -480,19 +609,144 @@ } | ||
HubConnection.prototype.connectionClosed = function (error) { | ||
this.logger.log(LogLevel.Debug, "HubConnection.connectionClosed(" + error + ") called while in state " + this.connectionState + "."); | ||
// Triggering this.handshakeRejecter is insufficient because it could already be resolved without the continuation having run yet. | ||
this.stopDuringStartError = this.stopDuringStartError || error || new Error("The underlying connection was closed before the hub handshake could complete."); | ||
// If the handshake is in progress, start will be waiting for the handshake promise, so we complete it. | ||
// If it has already completed, this should just noop. | ||
if (this.handshakeResolver) { | ||
this.handshakeResolver(); | ||
} | ||
this.cancelCallbacksWithError(error || new Error("Invocation canceled due to the underlying connection being closed.")); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
if (this.connectionState === HubConnectionState.Disconnecting) { | ||
this.completeClose(error); | ||
} | ||
else if (this.connectionState === HubConnectionState.Connected && this.reconnectPolicy) { | ||
// tslint:disable-next-line:no-floating-promises | ||
this.reconnect(error); | ||
} | ||
else if (this.connectionState === HubConnectionState.Connected) { | ||
this.completeClose(error); | ||
} | ||
// If none of the above if conditions were true were called the HubConnection must be in either: | ||
// 1. The Connecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail it. | ||
// 2. The Reconnecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail the current reconnect attempt | ||
// and potentially continue the reconnect() loop. | ||
// 3. The Disconnected state in which case we're already done. | ||
}; | ||
HubConnection.prototype.completeClose = function (error) { | ||
var _this = this; | ||
if (this.connectionStarted) { | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
try { | ||
this.closedCallbacks.forEach(function (c) { return c.apply(_this, [error]); }); | ||
} | ||
catch (e) { | ||
this.logger.log(LogLevel.Error, "An onclose callback called with error '" + error + "' threw error '" + e + "'."); | ||
} | ||
} | ||
}; | ||
HubConnection.prototype.reconnect = function (error) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var reconnectStartTime, previousReconnectAttempts, nextRetryDelay, e_4; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
reconnectStartTime = Date.now(); | ||
previousReconnectAttempts = 0; | ||
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, 0); | ||
if (nextRetryDelay === null) { | ||
this.logger.log(LogLevel.Debug, "Connection not reconnecting because the IReconnectPolicy returned null on the first reconnect attempt."); | ||
this.completeClose(error); | ||
return [2 /*return*/]; | ||
} | ||
this.connectionState = HubConnectionState.Reconnecting; | ||
if (error) { | ||
this.logger.log(LogLevel.Information, "Connection reconnecting because of error '" + error + "'."); | ||
} | ||
else { | ||
this.logger.log(LogLevel.Information, "Connection reconnecting."); | ||
} | ||
if (this.onreconnecting) { | ||
try { | ||
this.reconnectingCallbacks.forEach(function (c) { return c.apply(_this, [error]); }); | ||
} | ||
catch (e) { | ||
this.logger.log(LogLevel.Error, "An onreconnecting callback called with error '" + error + "' threw error '" + e + "'."); | ||
} | ||
// Exit early if an onreconnecting callback called connection.stop(). | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state in onreconnecting callback. Done reconnecting."); | ||
return [2 /*return*/]; | ||
} | ||
} | ||
_a.label = 1; | ||
case 1: | ||
if (!(nextRetryDelay !== null)) return [3 /*break*/, 7]; | ||
this.logger.log(LogLevel.Information, "Reconnect attempt number " + previousReconnectAttempts + " will start in " + nextRetryDelay + " ms."); | ||
return [4 /*yield*/, new Promise(function (resolve) { | ||
_this.reconnectDelayHandle = setTimeout(resolve, nextRetryDelay); | ||
})]; | ||
case 2: | ||
_a.sent(); | ||
this.reconnectDelayHandle = undefined; | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect delay. Done reconnecting."); | ||
return [2 /*return*/]; | ||
} | ||
_a.label = 3; | ||
case 3: | ||
_a.trys.push([3, 5, , 6]); | ||
return [4 /*yield*/, this.startInternal()]; | ||
case 4: | ||
_a.sent(); | ||
this.connectionState = HubConnectionState.Connected; | ||
this.logger.log(LogLevel.Information, "HubConnection reconnected successfully."); | ||
if (this.onreconnected) { | ||
try { | ||
this.reconnectedCallbacks.forEach(function (c) { return c.apply(_this, [_this.connection.connectionId]); }); | ||
} | ||
catch (e) { | ||
this.logger.log(LogLevel.Error, "An onreconnected callback called with connectionId '" + this.connection.connectionId + "; threw error '" + e + "'."); | ||
} | ||
} | ||
return [2 /*return*/]; | ||
case 5: | ||
e_4 = _a.sent(); | ||
this.logger.log(LogLevel.Information, "Reconnect attempt failed because of error '" + e_4 + "'."); | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect attempt. Done reconnecting."); | ||
return [2 /*return*/]; | ||
} | ||
return [3 /*break*/, 6]; | ||
case 6: | ||
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, Date.now() - reconnectStartTime); | ||
return [3 /*break*/, 1]; | ||
case 7: | ||
this.logger.log(LogLevel.Information, "Reconnect retries have been exhausted after " + (Date.now() - reconnectStartTime) + " ms and " + previousReconnectAttempts + " failed attempts. Connection disconnecting."); | ||
this.completeClose(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
HubConnection.prototype.getNextRetryDelay = function (previousRetryCount, elapsedMilliseconds) { | ||
try { | ||
return this.reconnectPolicy.nextRetryDelayInMilliseconds(previousRetryCount, elapsedMilliseconds); | ||
} | ||
catch (e) { | ||
this.logger.log(LogLevel.Error, "IReconnectPolicy.nextRetryDelayInMilliseconds(" + previousRetryCount + ", " + elapsedMilliseconds + ") threw error '" + e + "'."); | ||
return null; | ||
} | ||
}; | ||
HubConnection.prototype.cancelCallbacksWithError = function (error) { | ||
var callbacks = this.callbacks; | ||
this.callbacks = {}; | ||
this.connectionState = HubConnectionState.Disconnected; | ||
// if handshake is in progress start will be waiting for the handshake promise, so we complete it | ||
// if it has already completed this should just noop | ||
if (this.handshakeRejecter) { | ||
this.handshakeRejecter(error); | ||
} | ||
Object.keys(callbacks) | ||
.forEach(function (key) { | ||
var callback = callbacks[key]; | ||
callback(null, error ? error : new Error("Invocation canceled due to connection being closed.")); | ||
callback(null, error); | ||
}); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
this.closedCallbacks.forEach(function (c) { return c.apply(_this, [error]); }); | ||
}; | ||
@@ -499,0 +753,0 @@ HubConnection.prototype.cleanupPingTimer = function () { |
@@ -5,2 +5,3 @@ import { HubConnection } from "./HubConnection"; | ||
import { ILogger, LogLevel } from "./ILogger"; | ||
import { IReconnectPolicy } from "./IReconnectPolicy"; | ||
import { HttpTransportType } from "./ITransport"; | ||
@@ -54,2 +55,17 @@ /** A builder for configuring {@link @aspnet/signalr.HubConnection} instances. */ | ||
withHubProtocol(protocol: IHubProtocol): HubConnectionBuilder; | ||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost. | ||
* By default, the client will wait 0, 2, 10 and 30 seconds respectively before trying up to 4 reconnect attempts. | ||
*/ | ||
withAutomaticReconnect(): HubConnectionBuilder; | ||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost. | ||
* | ||
* @param {number[]} retryDelays An array containing the delays in milliseconds before trying each reconnect attempt. | ||
* The length of the array represents how many failed reconnect attempts it takes before the client will stop attempting to reconnect. | ||
*/ | ||
withAutomaticReconnect(retryDelays: number[]): HubConnectionBuilder; | ||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost. | ||
* | ||
* @param {number[]} reconnectPolicy An {@link @aspnet/signalR.IReconnectPolicy} that controls the timing and number of reconnect attempts. | ||
*/ | ||
withAutomaticReconnect(reconnectPolicy: IReconnectPolicy): HubConnectionBuilder; | ||
/** Creates a {@link @aspnet/signalr.HubConnection} from the configuration options specified in this builder. | ||
@@ -56,0 +72,0 @@ * |
// 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. | ||
var __assign = (this && this.__assign) || Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
import { DefaultReconnectPolicy } from "./DefaultReconnectPolicy"; | ||
import { HttpConnection } from "./HttpConnection"; | ||
@@ -28,8 +37,6 @@ import { HubConnection } from "./HubConnection"; | ||
if (typeof transportTypeOrOptions === "object") { | ||
this.httpConnectionOptions = transportTypeOrOptions; | ||
this.httpConnectionOptions = __assign({}, this.httpConnectionOptions, transportTypeOrOptions); | ||
} | ||
else { | ||
this.httpConnectionOptions = { | ||
transport: transportTypeOrOptions, | ||
}; | ||
this.httpConnectionOptions = __assign({}, this.httpConnectionOptions, { transport: transportTypeOrOptions }); | ||
} | ||
@@ -47,2 +54,17 @@ return this; | ||
}; | ||
HubConnectionBuilder.prototype.withAutomaticReconnect = function (retryDelaysOrReconnectPolicy) { | ||
if (this.reconnectPolicy) { | ||
throw new Error("A reconnectPolicy has already been set."); | ||
} | ||
if (!retryDelaysOrReconnectPolicy) { | ||
this.reconnectPolicy = new DefaultReconnectPolicy(); | ||
} | ||
else if (Array.isArray(retryDelaysOrReconnectPolicy)) { | ||
this.reconnectPolicy = new DefaultReconnectPolicy(retryDelaysOrReconnectPolicy); | ||
} | ||
else { | ||
this.reconnectPolicy = retryDelaysOrReconnectPolicy; | ||
} | ||
return this; | ||
}; | ||
/** Creates a {@link @aspnet/signalr.HubConnection} from the configuration options specified in this builder. | ||
@@ -66,3 +88,3 @@ * | ||
var connection = new HttpConnection(this.url, httpConnectionOptions); | ||
return HubConnection.create(connection, this.logger || NullLogger.instance, this.protocol || new JsonHubProtocol()); | ||
return HubConnection.create(connection, this.logger || NullLogger.instance, this.protocol || new JsonHubProtocol(), this.reconnectPolicy); | ||
}; | ||
@@ -69,0 +91,0 @@ return HubConnectionBuilder; |
@@ -5,2 +5,3 @@ import { TransferFormat } from "./ITransport"; | ||
readonly features: any; | ||
readonly connectionId?: string; | ||
start(transferFormat: TransferFormat): Promise<void>; | ||
@@ -7,0 +8,0 @@ send(data: string | ArrayBuffer): Promise<void>; |
@@ -17,3 +17,4 @@ /** The version of the SignalR client. */ | ||
export { Subject } from "./Subject"; | ||
export { IReconnectPolicy } from "./IReconnectPolicy"; | ||
export as namespace signalR; |
@@ -5,3 +5,3 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
/** The version of the SignalR client. */ | ||
export var VERSION = "3.0.0-preview3-19153-02"; | ||
export var VERSION = "3.0.0-preview4-19216-03"; | ||
export { AbortError, HttpError, TimeoutError } from "./Errors"; | ||
@@ -8,0 +8,0 @@ export { HttpClient, HttpResponse } from "./HttpClient"; |
@@ -21,3 +21,2 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
}; | ||
import * as Request from "request"; | ||
import { AbortError, HttpError, TimeoutError } from "./Errors"; | ||
@@ -27,2 +26,9 @@ import { HttpClient, HttpResponse } from "./HttpClient"; | ||
import { isArrayBuffer } from "./Utils"; | ||
var requestModule; | ||
if (typeof XMLHttpRequest === "undefined") { | ||
// In order to ignore the dynamic require in webpack builds we need to do this magic | ||
// @ts-ignore: TS doesn't know about these names | ||
var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; | ||
requestModule = requireFunc("request"); | ||
} | ||
var NodeHttpClient = /** @class */ (function (_super) { | ||
@@ -32,5 +38,8 @@ __extends(NodeHttpClient, _super); | ||
var _this = _super.call(this) || this; | ||
if (typeof requestModule === "undefined") { | ||
throw new Error("The 'request' module could not be loaded."); | ||
} | ||
_this.logger = logger; | ||
_this.cookieJar = Request.jar(); | ||
_this.request = Request.defaults({ jar: _this.cookieJar }); | ||
_this.cookieJar = requestModule.jar(); | ||
_this.request = requestModule.defaults({ jar: _this.cookieJar }); | ||
return _this; | ||
@@ -37,0 +46,0 @@ } |
{ | ||
"name": "@aspnet/signalr", | ||
"version": "3.0.0-preview3-19153-02", | ||
"version": "3.0.0-preview4-19216-03", | ||
"description": "ASP.NET Core SignalR Client", | ||
@@ -42,12 +42,12 @@ "main": "./dist/cjs/index.js", | ||
"devDependencies": { | ||
"es6-promise": "^4.2.2", | ||
"@types/eventsource": "^1.0.2", | ||
"@types/node": "^10.9.4", | ||
"@types/eventsource": "^1.0.2", | ||
"@types/request": "^2.47.1" | ||
"@types/request": "^2.47.1", | ||
"es6-promise": "^4.2.2" | ||
}, | ||
"dependencies": { | ||
"ws": "^6.0.0", | ||
"eventsource": "^1.0.7", | ||
"request": "^2.88.0" | ||
"request": "^2.88.0", | ||
"ws": "^6.0.0" | ||
} | ||
} |
@@ -8,2 +8,6 @@ JavaScript and TypeScript clients for SignalR for ASP.NET Core | ||
``` | ||
or | ||
```bash | ||
yarn add @aspnet/signalr | ||
``` | ||
@@ -10,0 +14,0 @@ ## Usage |
@@ -7,12 +7,5 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
import { ILogger } from "./ILogger"; | ||
import { NodeHttpClient } from "./NodeHttpClient"; | ||
import { XhrHttpClient } from "./XhrHttpClient"; | ||
let nodeHttpClientModule: any; | ||
if (typeof XMLHttpRequest === "undefined") { | ||
// In order to ignore the dynamic require in webpack builds we need to do this magic | ||
// @ts-ignore: TS doesn't know about these names | ||
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; | ||
nodeHttpClientModule = requireFunc("./NodeHttpClient"); | ||
} | ||
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */ | ||
@@ -28,6 +21,4 @@ export class DefaultHttpClient extends HttpClient { | ||
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."); | ||
this.httpClient = new NodeHttpClient(logger); | ||
} | ||
@@ -34,0 +25,0 @@ } |
@@ -17,5 +17,6 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
const enum ConnectionState { | ||
Connecting, | ||
Connected, | ||
Disconnected, | ||
Connecting = "Connecting ", | ||
Connected = "Connected", | ||
Disconnected = "Disconnected", | ||
Disconnecting = "Disconnecting", | ||
} | ||
@@ -53,3 +54,6 @@ | ||
private connectionState: ConnectionState; | ||
private baseUrl: string; | ||
// connectionStarted is tracked independently from connectionState, so we can check if the | ||
// connection ever did successfully transition from connecting to connected before disconnecting. | ||
private connectionStarted: boolean; | ||
private readonly baseUrl: string; | ||
private readonly httpClient: HttpClient; | ||
@@ -59,3 +63,5 @@ private readonly logger: ILogger; | ||
private transport?: ITransport; | ||
private startPromise?: Promise<void>; | ||
private startInternalPromise?: Promise<void>; | ||
private stopPromise?: Promise<void>; | ||
private stopPromiseResolver!: (value?: PromiseLike<void>) => void; | ||
private stopError?: Error; | ||
@@ -65,2 +71,3 @@ private accessTokenFactory?: () => string | Promise<string>; | ||
public readonly features: any = {}; | ||
public connectionId?: string; | ||
public onreceive: ((data: string | ArrayBuffer) => void) | null; | ||
@@ -96,3 +103,5 @@ public onclose: ((e?: Error) => void) | null; | ||
this.connectionState = ConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
this.options = options; | ||
this.onreceive = null; | ||
@@ -104,3 +113,3 @@ this.onclose = null; | ||
public start(transferFormat: TransferFormat): Promise<void>; | ||
public start(transferFormat?: TransferFormat): Promise<void> { | ||
public async start(transferFormat?: TransferFormat): Promise<void> { | ||
transferFormat = transferFormat || TransferFormat.Binary; | ||
@@ -113,3 +122,3 @@ | ||
if (this.connectionState !== ConnectionState.Disconnected) { | ||
return Promise.reject(new Error("Cannot start a connection that is not in the 'Disconnected' state.")); | ||
return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state.")); | ||
} | ||
@@ -119,4 +128,23 @@ | ||
this.startPromise = this.startInternal(transferFormat); | ||
return this.startPromise; | ||
this.startInternalPromise = this.startInternal(transferFormat); | ||
await this.startInternalPromise; | ||
// The TypeScript compiler thinks that connectionState must be Connecting here. The TypeScript compiler is wrong. | ||
if (this.connectionState as any === ConnectionState.Disconnecting) { | ||
// stop() was called and transitioned the client into the Disconnecting state. | ||
const message = "Failed to start the HttpConnection before stop() was called."; | ||
this.logger.log(LogLevel.Error, message); | ||
// We cannot await stopPromise inside startInternal since stopInternal awaits the startInternalPromise. | ||
await this.stopPromise; | ||
return Promise.reject(new Error(message)); | ||
} else if (this.connectionState as any !== ConnectionState.Connected) { | ||
// stop() was called and transitioned the client into the Disconnecting state. | ||
const message = "HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!"; | ||
this.logger.log(LogLevel.Error, message); | ||
return Promise.reject(new Error(message)); | ||
} | ||
this.connectionStarted = true; | ||
} | ||
@@ -126,3 +154,3 @@ | ||
if (this.connectionState !== ConnectionState.Connected) { | ||
throw new Error("Cannot send data if the connection is not in the 'Connected' State."); | ||
return Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")); | ||
} | ||
@@ -135,3 +163,25 @@ | ||
public async stop(error?: Error): Promise<void> { | ||
this.connectionState = ConnectionState.Disconnected; | ||
if (this.connectionState === ConnectionState.Disconnected) { | ||
this.logger.log(LogLevel.Debug, `Call to HttpConnection.stop(${error}) ignored because the connection is already in the disconnected state.`); | ||
return Promise.resolve(); | ||
} | ||
if (this.connectionState === ConnectionState.Disconnecting) { | ||
this.logger.log(LogLevel.Debug, `Call to HttpConnection.stop(${error}) ignored because the connection is already in the disconnecting state.`); | ||
return this.stopPromise; | ||
} | ||
this.connectionState = ConnectionState.Disconnecting; | ||
this.stopPromise = new Promise((resolve) => { | ||
// Don't complete stop() until stopConnection() completes. | ||
this.stopPromiseResolver = resolve; | ||
}); | ||
// stopInternal should never throw so just observe it. | ||
await this.stopInternal(error); | ||
await this.stopPromise; | ||
} | ||
private async stopInternal(error?: Error): Promise<void> { | ||
// Set error as soon as possible otherwise there is a race between | ||
@@ -143,11 +193,22 @@ // the transport closing and providing an error and the error from a close message | ||
try { | ||
await this.startPromise; | ||
await this.startInternalPromise; | ||
} catch (e) { | ||
// this exception is returned to the user as a rejected Promise from the start method | ||
// This exception is returned to the user as a rejected Promise from the start method. | ||
} | ||
// The transport's onclose will trigger stopConnection which will run our onclose event. | ||
// The transport should always be set if currently connected. If it wasn't set, it's likely because | ||
// stop was called during start() and start() failed. | ||
if (this.transport) { | ||
await this.transport.stop(); | ||
try { | ||
await this.transport.stop(); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `HttpConnection.transport.stop() threw error '${e}'.`); | ||
this.stopConnection(); | ||
} | ||
this.transport = undefined; | ||
} else { | ||
this.logger.log(LogLevel.Debug, "HttpConnection.transport is undefined in HttpConnection.stop() because start() failed."); | ||
this.stopConnection(); | ||
} | ||
@@ -171,3 +232,3 @@ } | ||
} else { | ||
throw Error("Negotiation can only be skipped when using the WebSocket transport directly."); | ||
throw new Error("Negotiation can only be skipped when using the WebSocket transport directly."); | ||
} | ||
@@ -181,12 +242,12 @@ } else { | ||
// the user tries to stop the connection when it is being started | ||
if (this.connectionState === ConnectionState.Disconnected) { | ||
return; | ||
if (this.connectionState === ConnectionState.Disconnecting || this.connectionState === ConnectionState.Disconnected) { | ||
throw new Error("The connection was stopped during negotiation."); | ||
} | ||
if (negotiateResponse.error) { | ||
throw Error(negotiateResponse.error); | ||
throw new Error(negotiateResponse.error); | ||
} | ||
if ((negotiateResponse as any).ProtocolVersion) { | ||
throw Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); | ||
throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); | ||
} | ||
@@ -210,5 +271,7 @@ | ||
if (redirects === MAX_REDIRECTS && negotiateResponse.url) { | ||
throw Error("Negotiate redirection limit exceeded."); | ||
throw new Error("Negotiate redirection limit exceeded."); | ||
} | ||
this.connectionId = negotiateResponse.connectionId; | ||
await this.createTransport(url, this.options.transport, negotiateResponse, transferFormat); | ||
@@ -224,5 +287,12 @@ } | ||
// 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); | ||
if (this.connectionState === ConnectionState.Connecting) { | ||
// Ensure the connection transitions to the connected state prior to completing this.startInternalPromise. | ||
// start() will handle the case when stop was called and startInternal exits still in the disconnecting state. | ||
this.logger.log(LogLevel.Debug, "The HttpConnection connected successfully."); | ||
this.connectionState = ConnectionState.Connected; | ||
} | ||
// stop() is waiting on us via this.startInternalPromise so keep this.transport around so it can clean up. | ||
// This is the only case startInternal can exit in neither the connected nor disconnected state because stopConnection() | ||
// will transition to the disconnected state. start() will wait for the transition using the stopPromise. | ||
} catch (e) { | ||
@@ -232,3 +302,3 @@ this.logger.log(LogLevel.Error, "Failed to start the connection: " + e); | ||
this.transport = undefined; | ||
throw e; | ||
return Promise.reject(e); | ||
} | ||
@@ -257,3 +327,3 @@ } | ||
if (response.statusCode !== 200) { | ||
throw Error(`Unexpected status code returned from negotiate ${response.statusCode}`); | ||
return Promise.reject(new Error(`Unexpected status code returned from negotiate ${response.statusCode}`)); | ||
} | ||
@@ -264,3 +334,3 @@ | ||
this.logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e); | ||
throw e; | ||
return Promise.reject(e); | ||
} | ||
@@ -283,5 +353,2 @@ } | ||
// 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); | ||
return; | ||
@@ -294,3 +361,2 @@ } | ||
try { | ||
this.connectionState = ConnectionState.Connecting; | ||
const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); | ||
@@ -304,3 +370,2 @@ if (typeof transport === "number") { | ||
await this.transport!.connect(connectUrl, requestedTransferFormat); | ||
this.changeState(ConnectionState.Connecting, ConnectionState.Connected); | ||
return; | ||
@@ -310,5 +375,10 @@ } | ||
this.logger.log(LogLevel.Error, `Failed to start the transport '${endpoint.transport}': ${ex}`); | ||
this.connectionState = ConnectionState.Disconnected; | ||
negotiateResponse.connectionId = undefined; | ||
transportExceptions.push(`${endpoint.transport} failed: ${ex}`); | ||
if (this.connectionState !== ConnectionState.Connecting) { | ||
const message = "Failed to select transport before stop() was called."; | ||
this.logger.log(LogLevel.Debug, message); | ||
return Promise.reject(new Error(message)); | ||
} | ||
} | ||
@@ -318,5 +388,5 @@ } | ||
if (transportExceptions.length > 0) { | ||
throw new Error(`Unable to connect to the server with any of the available transports. ${transportExceptions.join(" ")}`); | ||
return Promise.reject(new Error(`Unable to connect to the server with any of the available transports. ${transportExceptions.join(" ")}`)); | ||
} | ||
throw new Error("None of the transports supported by the client are supported by the server."); | ||
return Promise.reject(new Error("None of the transports supported by the client are supported by the server.")); | ||
} | ||
@@ -375,11 +445,5 @@ | ||
private changeState(from: ConnectionState, to: ConnectionState): boolean { | ||
if (this.connectionState === from) { | ||
this.connectionState = to; | ||
return true; | ||
} | ||
return false; | ||
} | ||
private stopConnection(error?: Error): void { | ||
this.logger.log(LogLevel.Debug, `HttpConnection.stopConnection(${error}) called while in state ${this.connectionState}.`); | ||
private stopConnection(error?: Error): void { | ||
this.transport = undefined; | ||
@@ -389,3 +453,20 @@ | ||
error = this.stopError || error; | ||
this.stopError = undefined; | ||
if (this.connectionState === ConnectionState.Disconnected) { | ||
this.logger.log(LogLevel.Debug, `Call to HttpConnection.stopConnection(${error}) was ignored because the connection is already in the disconnected state.`); | ||
return; | ||
} | ||
if (this.connectionState === ConnectionState.Connecting) { | ||
this.logger.log(LogLevel.Warning, `Call to HttpConnection.stopConnection(${error}) was ignored because the connection hasn't yet left the in the connecting state.`); | ||
return; | ||
} | ||
if (this.connectionState === ConnectionState.Disconnecting) { | ||
// A call to stop() induced this call to stopConnection and needs to be completed. | ||
// Any stop() awaiters will be scheduled to continue after the onclose callback fires. | ||
this.stopPromiseResolver(); | ||
} | ||
if (error) { | ||
@@ -399,4 +480,10 @@ this.logger.log(LogLevel.Error, `Connection disconnected with error '${error}'.`); | ||
if (this.onclose) { | ||
this.onclose(error); | ||
if (this.onclose && this.connectionStarted) { | ||
this.connectionStarted = false; | ||
try { | ||
this.onclose(error); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `HttpConnection.onclose(${error}) threw error '${e}'.`); | ||
} | ||
} | ||
@@ -403,0 +490,0 @@ } |
@@ -8,2 +8,3 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
import { ILogger, LogLevel } from "./ILogger"; | ||
import { IReconnectPolicy } from "./IReconnectPolicy"; | ||
import { IStreamResult } from "./Stream"; | ||
@@ -19,5 +20,11 @@ import { Subject } from "./Subject"; | ||
/** The hub connection is disconnected. */ | ||
Disconnected, | ||
Disconnected = "Disconnected", | ||
/** The hub connection is connecting. */ | ||
Connecting = "Connecting", | ||
/** The hub connection is connected. */ | ||
Connected, | ||
Connected = "Connected", | ||
/** The hub connection is disconnecting. */ | ||
Disconnecting = "Disconnecting", | ||
/** The hub connection is reconnecting. */ | ||
Reconnecting = "Reconnecting", | ||
} | ||
@@ -30,2 +37,3 @@ | ||
private readonly logger: ILogger; | ||
private readonly reconnectPolicy?: IReconnectPolicy; | ||
private protocol: IHubProtocol; | ||
@@ -36,7 +44,18 @@ private handshakeProtocol: HandshakeProtocol; | ||
private invocationId: number; | ||
private closedCallbacks: Array<(error?: Error) => void>; | ||
private reconnectingCallbacks: Array<(error?: Error) => void>; | ||
private reconnectedCallbacks: Array<(connectionId?: string) => void>; | ||
private receivedHandshakeResponse: boolean; | ||
private handshakeResolver!: (value?: PromiseLike<{}>) => void; | ||
private handshakeRejecter!: (reason?: any) => void; | ||
private stopDuringStartError?: Error; | ||
private connectionState: HubConnectionState; | ||
// connectionStarted is tracked independently from connectionState, so we can check if the | ||
// connection ever did successfully transition from connecting to connected before disconnecting. | ||
private connectionStarted: boolean; | ||
private startPromise?: Promise<void>; | ||
private stopPromise?: Promise<void>; | ||
@@ -46,2 +65,3 @@ // The type of these a) doesn't matter and b) varies when building in browser and node contexts | ||
// we built the bundle from the compiled JavaScript). | ||
private reconnectDelayHandle?: any; | ||
private timeoutHandle?: any; | ||
@@ -69,7 +89,7 @@ private pingServerHandle?: any; | ||
// public parameter-less constructor. | ||
public static create(connection: IConnection, logger: ILogger, protocol: IHubProtocol): HubConnection { | ||
return new HubConnection(connection, logger, protocol); | ||
public static create(connection: IConnection, logger: ILogger, protocol: IHubProtocol, reconnectPolicy?: IReconnectPolicy): HubConnection { | ||
return new HubConnection(connection, logger, protocol, reconnectPolicy); | ||
} | ||
private constructor(connection: IConnection, logger: ILogger, protocol: IHubProtocol) { | ||
private constructor(connection: IConnection, logger: ILogger, protocol: IHubProtocol, reconnectPolicy?: IReconnectPolicy) { | ||
Arg.isRequired(connection, "connection"); | ||
@@ -85,2 +105,3 @@ Arg.isRequired(logger, "logger"); | ||
this.connection = connection; | ||
this.reconnectPolicy = reconnectPolicy; | ||
this.handshakeProtocol = new HandshakeProtocol(); | ||
@@ -94,5 +115,8 @@ | ||
this.closedCallbacks = []; | ||
this.reconnectingCallbacks = []; | ||
this.reconnectedCallbacks = []; | ||
this.invocationId = 0; | ||
this.receivedHandshakeResponse = false; | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
@@ -111,12 +135,32 @@ this.cachedPingMessage = this.protocol.writeMessage({ type: MessageType.Ping }); | ||
*/ | ||
public async start(): Promise<void> { | ||
const handshakeRequest: HandshakeRequestMessage = { | ||
protocol: this.protocol.name, | ||
version: this.protocol.version, | ||
}; | ||
public start(): Promise<void> { | ||
this.startPromise = this.startWithStateTransitions(); | ||
return this.startPromise; | ||
} | ||
private async startWithStateTransitions(): Promise<void> { | ||
if (this.connectionState !== HubConnectionState.Disconnected) { | ||
return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state.")); | ||
} | ||
this.connectionState = HubConnectionState.Connecting; | ||
this.logger.log(LogLevel.Debug, "Starting HubConnection."); | ||
try { | ||
await this.startInternal(); | ||
this.connectionState = HubConnectionState.Connected; | ||
this.connectionStarted = true; | ||
this.logger.log(LogLevel.Debug, "HubConnection connected successfully."); | ||
} catch (e) { | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.logger.log(LogLevel.Debug, `HubConnection failed to start successfully because of error '${e}'.`); | ||
return Promise.reject(e); | ||
} | ||
} | ||
private async startInternal() { | ||
this.stopDuringStartError = undefined; | ||
this.receivedHandshakeResponse = false; | ||
// Set up the promise before any connection is started otherwise it could race with received messages | ||
// Set up the promise before any connection is (re)started otherwise it could race with received messages | ||
const handshakePromise = new Promise((resolve, reject) => { | ||
@@ -129,16 +173,35 @@ this.handshakeResolver = resolve; | ||
this.logger.log(LogLevel.Debug, "Sending handshake request."); | ||
try { | ||
const handshakeRequest: HandshakeRequestMessage = { | ||
protocol: this.protocol.name, | ||
version: this.protocol.version, | ||
}; | ||
await this.sendMessage(this.handshakeProtocol.writeHandshakeRequest(handshakeRequest)); | ||
this.logger.log(LogLevel.Debug, "Sending handshake request."); | ||
this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`); | ||
await this.sendMessage(this.handshakeProtocol.writeHandshakeRequest(handshakeRequest)); | ||
// defensively cleanup timeout in case we receive a message from the server before we finish start | ||
this.cleanupTimeout(); | ||
this.resetTimeoutPeriod(); | ||
this.resetKeepAliveInterval(); | ||
this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`); | ||
// Wait for the handshake to complete before marking connection as connected | ||
await handshakePromise; | ||
this.connectionState = HubConnectionState.Connected; | ||
// defensively cleanup timeout in case we receive a message from the server before we finish start | ||
this.cleanupTimeout(); | ||
this.resetTimeoutPeriod(); | ||
this.resetKeepAliveInterval(); | ||
await handshakePromise; | ||
if (this.stopDuringStartError) { | ||
throw this.stopDuringStartError; | ||
} | ||
} catch (e) { | ||
this.logger.log(LogLevel.Debug, `Hub handshake failed with error '${e}' during start(). Stopping HubConnection.`); | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
// HttpConnection.stop() should not complete until after the onclose callback is invoked. | ||
// This will transition the HubConnection to the disconnected state before HttpConnection.stop() completes. | ||
await this.connection.stop(e); | ||
throw e; | ||
} | ||
} | ||
@@ -150,8 +213,53 @@ | ||
*/ | ||
public stop(): Promise<void> { | ||
public async stop(): Promise<void> { | ||
// Capture the start promise before the connection might be restarted in an onclose callback. | ||
const startPromise = this.startPromise; | ||
this.stopPromise = this.stopInternal(); | ||
await this.stopPromise; | ||
try { | ||
// Awaiting undefined continues immediately | ||
await startPromise; | ||
} catch (e) { | ||
// This exception is returned to the user as a rejected Promise from the start method. | ||
} | ||
} | ||
private stopInternal(error?: Error): Promise<void> { | ||
if (this.connectionState === HubConnectionState.Disconnected) { | ||
this.logger.log(LogLevel.Debug, `Call to HubConnection.stop(${error}) ignored because it is already in the disconnected state.`); | ||
return Promise.resolve(); | ||
} | ||
if (this.connectionState === HubConnectionState.Disconnecting) { | ||
this.logger.log(LogLevel.Debug, `Call to HttpConnection.stop(${error}) ignored because the connection is already in the disconnecting state.`); | ||
return this.stopPromise!; | ||
} | ||
this.connectionState = HubConnectionState.Disconnecting; | ||
this.logger.log(LogLevel.Debug, "Stopping HubConnection."); | ||
if (this.reconnectDelayHandle) { | ||
// We're in a reconnect delay which means the underlying connection is currently already stopped. | ||
// Just clear the handle to stop the reconnect loop (which no one is waiting on thankfully) and | ||
// fire the onclose callbacks. | ||
this.logger.log(LogLevel.Debug, "Connection stopped during reconnect delay. Done reconnecting."); | ||
clearTimeout(this.reconnectDelayHandle); | ||
this.reconnectDelayHandle = undefined; | ||
this.completeClose(); | ||
return Promise.resolve(); | ||
} | ||
this.cleanupTimeout(); | ||
this.cleanupPingTimer(); | ||
return this.connection.stop(); | ||
this.stopDuringStartError = error || new Error("The connection was stopped before the hub handshake could complete."); | ||
// HttpConnection.stop() should not complete until after either HttpConnection.start() fails | ||
// or the onclose callback is invoked. The onclose callback will transition the HubConnection | ||
// to the disconnected state if need be before HttpConnection.stop() completes. | ||
return this.connection.stop(error); | ||
} | ||
@@ -362,2 +470,22 @@ | ||
/** Registers a handler that will be invoked when the connection starts reconnecting. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection starts reconnecting. Optionally receives a single argument containing the error that caused the connection to start reconnecting (if any). | ||
*/ | ||
public onreconnecting(callback: (error?: Error) => void) { | ||
if (callback) { | ||
this.reconnectingCallbacks.push(callback); | ||
} | ||
} | ||
/** Registers a handler that will be invoked when the connection successfully reconnects. | ||
* | ||
* @param {Function} callback The handler that will be invoked when the connection successfully reconnects. | ||
*/ | ||
public onreconnected(callback: (connectionId?: string) => void) { | ||
if (callback) { | ||
this.reconnectedCallbacks.push(callback); | ||
} | ||
} | ||
private processIncomingData(data: any) { | ||
@@ -384,3 +512,3 @@ this.cleanupTimeout(); | ||
const callback = this.callbacks[message.invocationId]; | ||
if (callback != null) { | ||
if (callback) { | ||
if (message.type === MessageType.Completion) { | ||
@@ -399,4 +527,3 @@ delete this.callbacks[message.invocationId]; | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(message.error ? new Error("Server returned an error on close: " + message.error) : undefined); | ||
this.stopPromise = this.stopInternal(message.error ? new Error("Server returned an error on close: " + message.error) : undefined); | ||
@@ -425,6 +552,2 @@ break; | ||
const error = new Error(message); | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(error); | ||
this.handshakeRejecter(error); | ||
@@ -437,7 +560,5 @@ throw error; | ||
this.handshakeRejecter(message); | ||
// We don't want to wait on the stop itself. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(new Error(message)); | ||
throw new Error(message); | ||
const error = new Error(message); | ||
this.handshakeRejecter(error); | ||
throw error; | ||
} else { | ||
@@ -475,3 +596,3 @@ this.logger.log(LogLevel.Debug, "Server handshake complete."); | ||
// The server hasn't talked to us in a while. It doesn't like us anymore ... :( | ||
// Terminate the connection, but we don't need to wait on the promise. | ||
// Terminate the connection, but we don't need to wait on the promise. This could trigger reconnecting. | ||
// tslint:disable-next-line:no-floating-promises | ||
@@ -484,3 +605,8 @@ this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server.")); | ||
if (methods) { | ||
methods.forEach((m) => m.apply(this, invocationMessage.arguments)); | ||
try { | ||
methods.forEach((m) => m.apply(this, invocationMessage.arguments)); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `A callback for the method ${invocationMessage.target.toLowerCase()} threw error '${e}'.`); | ||
} | ||
if (invocationMessage.invocationId) { | ||
@@ -491,5 +617,4 @@ // This is not supported in v1. So we return an error to avoid blocking the server waiting for the response. | ||
// We don't need to wait on this Promise. | ||
// tslint:disable-next-line:no-floating-promises | ||
this.connection.stop(new Error(message)); | ||
// We don't want to wait on the stop itself. | ||
this.stopPromise = this.stopInternal(new Error(message)); | ||
} | ||
@@ -502,18 +627,14 @@ } else { | ||
private connectionClosed(error?: Error) { | ||
const callbacks = this.callbacks; | ||
this.callbacks = {}; | ||
this.logger.log(LogLevel.Debug, `HubConnection.connectionClosed(${error}) called while in state ${this.connectionState}.`); | ||
this.connectionState = HubConnectionState.Disconnected; | ||
// Triggering this.handshakeRejecter is insufficient because it could already be resolved without the continuation having run yet. | ||
this.stopDuringStartError = this.stopDuringStartError || error || new Error("The underlying connection was closed before the hub handshake could complete."); | ||
// if handshake is in progress start will be waiting for the handshake promise, so we complete it | ||
// if it has already completed this should just noop | ||
if (this.handshakeRejecter) { | ||
this.handshakeRejecter(error); | ||
// If the handshake is in progress, start will be waiting for the handshake promise, so we complete it. | ||
// If it has already completed, this should just noop. | ||
if (this.handshakeResolver) { | ||
this.handshakeResolver(); | ||
} | ||
Object.keys(callbacks) | ||
.forEach((key) => { | ||
const callback = callbacks[key]; | ||
callback(null, error ? error : new Error("Invocation canceled due to connection being closed.")); | ||
}); | ||
this.cancelCallbacksWithError(error || new Error("Invocation canceled due to the underlying connection being closed.")); | ||
@@ -523,5 +644,130 @@ this.cleanupTimeout(); | ||
this.closedCallbacks.forEach((c) => c.apply(this, [error])); | ||
if (this.connectionState === HubConnectionState.Disconnecting) { | ||
this.completeClose(error); | ||
} else if (this.connectionState === HubConnectionState.Connected && this.reconnectPolicy) { | ||
// tslint:disable-next-line:no-floating-promises | ||
this.reconnect(error); | ||
} else if (this.connectionState === HubConnectionState.Connected) { | ||
this.completeClose(error); | ||
} | ||
// If none of the above if conditions were true were called the HubConnection must be in either: | ||
// 1. The Connecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail it. | ||
// 2. The Reconnecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail the current reconnect attempt | ||
// and potentially continue the reconnect() loop. | ||
// 3. The Disconnected state in which case we're already done. | ||
} | ||
private completeClose(error?: Error) { | ||
if (this.connectionStarted) { | ||
this.connectionState = HubConnectionState.Disconnected; | ||
this.connectionStarted = false; | ||
try { | ||
this.closedCallbacks.forEach((c) => c.apply(this, [error])); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `An onclose callback called with error '${error}' threw error '${e}'.`); | ||
} | ||
} | ||
} | ||
private async reconnect(error?: Error) { | ||
const reconnectStartTime = Date.now(); | ||
let previousReconnectAttempts = 0; | ||
let nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, 0); | ||
if (nextRetryDelay === null) { | ||
this.logger.log(LogLevel.Debug, "Connection not reconnecting because the IReconnectPolicy returned null on the first reconnect attempt."); | ||
this.completeClose(error); | ||
return; | ||
} | ||
this.connectionState = HubConnectionState.Reconnecting; | ||
if (error) { | ||
this.logger.log(LogLevel.Information, `Connection reconnecting because of error '${error}'.`); | ||
} else { | ||
this.logger.log(LogLevel.Information, "Connection reconnecting."); | ||
} | ||
if (this.onreconnecting) { | ||
try { | ||
this.reconnectingCallbacks.forEach((c) => c.apply(this, [error])); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `An onreconnecting callback called with error '${error}' threw error '${e}'.`); | ||
} | ||
// Exit early if an onreconnecting callback called connection.stop(). | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state in onreconnecting callback. Done reconnecting."); | ||
return; | ||
} | ||
} | ||
while (nextRetryDelay !== null) { | ||
this.logger.log(LogLevel.Information, `Reconnect attempt number ${previousReconnectAttempts} will start in ${nextRetryDelay} ms.`); | ||
await new Promise((resolve) => { | ||
this.reconnectDelayHandle = setTimeout(resolve, nextRetryDelay!); | ||
}); | ||
this.reconnectDelayHandle = undefined; | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect delay. Done reconnecting."); | ||
return; | ||
} | ||
try { | ||
await this.startInternal(); | ||
this.connectionState = HubConnectionState.Connected; | ||
this.logger.log(LogLevel.Information, "HubConnection reconnected successfully."); | ||
if (this.onreconnected) { | ||
try { | ||
this.reconnectedCallbacks.forEach((c) => c.apply(this, [this.connection.connectionId])); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${e}'.`); | ||
} | ||
} | ||
return; | ||
} catch (e) { | ||
this.logger.log(LogLevel.Information, `Reconnect attempt failed because of error '${e}'.`); | ||
if (this.connectionState !== HubConnectionState.Reconnecting) { | ||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect attempt. Done reconnecting."); | ||
return; | ||
} | ||
} | ||
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, Date.now() - reconnectStartTime); | ||
} | ||
this.logger.log(LogLevel.Information, `Reconnect retries have been exhausted after ${Date.now() - reconnectStartTime} ms and ${previousReconnectAttempts} failed attempts. Connection disconnecting.`); | ||
this.completeClose(); | ||
} | ||
private getNextRetryDelay(previousRetryCount: number, elapsedMilliseconds: number) { | ||
try { | ||
return this.reconnectPolicy!.nextRetryDelayInMilliseconds(previousRetryCount, elapsedMilliseconds); | ||
} catch (e) { | ||
this.logger.log(LogLevel.Error, `IReconnectPolicy.nextRetryDelayInMilliseconds(${previousRetryCount}, ${elapsedMilliseconds}) threw error '${e}'.`); | ||
return null; | ||
} | ||
} | ||
private cancelCallbacksWithError(error: Error) { | ||
const callbacks = this.callbacks; | ||
this.callbacks = {}; | ||
Object.keys(callbacks) | ||
.forEach((key) => { | ||
const callback = callbacks[key]; | ||
callback(null, error); | ||
}); | ||
} | ||
private cleanupPingTimer(): void { | ||
@@ -528,0 +774,0 @@ if (this.pingServerHandle) { |
// 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 { DefaultReconnectPolicy } from "./DefaultReconnectPolicy"; | ||
import { HttpConnection } from "./HttpConnection"; | ||
@@ -9,2 +10,3 @@ import { HubConnection } from "./HubConnection"; | ||
import { ILogger, LogLevel } from "./ILogger"; | ||
import { IReconnectPolicy } from "./IReconnectPolicy"; | ||
import { HttpTransportType } from "./ITransport"; | ||
@@ -26,2 +28,6 @@ import { JsonHubProtocol } from "./JsonHubProtocol"; | ||
/** If defined, this indicates the client should automatically attempt to reconnect if the connection is lost. */ | ||
/** @internal */ | ||
public reconnectPolicy?: IReconnectPolicy; | ||
/** Configures console logging for the {@link @aspnet/signalr.HubConnection}. | ||
@@ -40,2 +46,3 @@ * | ||
public configureLogging(logger: ILogger): HubConnectionBuilder; | ||
/** Configures custom logging for the {@link @aspnet/signalr.HubConnection}. | ||
@@ -91,5 +98,6 @@ * | ||
if (typeof transportTypeOrOptions === "object") { | ||
this.httpConnectionOptions = transportTypeOrOptions; | ||
this.httpConnectionOptions = {...this.httpConnectionOptions, ...transportTypeOrOptions}; | ||
} else { | ||
this.httpConnectionOptions = { | ||
...this.httpConnectionOptions, | ||
transport: transportTypeOrOptions, | ||
@@ -113,2 +121,35 @@ }; | ||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost. | ||
* By default, the client will wait 0, 2, 10 and 30 seconds respectively before trying up to 4 reconnect attempts. | ||
*/ | ||
public withAutomaticReconnect(): HubConnectionBuilder; | ||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost. | ||
* | ||
* @param {number[]} retryDelays An array containing the delays in milliseconds before trying each reconnect attempt. | ||
* The length of the array represents how many failed reconnect attempts it takes before the client will stop attempting to reconnect. | ||
*/ | ||
public withAutomaticReconnect(retryDelays: number[]): HubConnectionBuilder; | ||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost. | ||
* | ||
* @param {number[]} reconnectPolicy An {@link @aspnet/signalR.IReconnectPolicy} that controls the timing and number of reconnect attempts. | ||
*/ | ||
public withAutomaticReconnect(reconnectPolicy: IReconnectPolicy): HubConnectionBuilder; | ||
public withAutomaticReconnect(retryDelaysOrReconnectPolicy?: number[] | IReconnectPolicy): HubConnectionBuilder { | ||
if (this.reconnectPolicy) { | ||
throw new Error("A reconnectPolicy has already been set."); | ||
} | ||
if (!retryDelaysOrReconnectPolicy) { | ||
this.reconnectPolicy = new DefaultReconnectPolicy(); | ||
} else if (Array.isArray(retryDelaysOrReconnectPolicy)) { | ||
this.reconnectPolicy = new DefaultReconnectPolicy(retryDelaysOrReconnectPolicy); | ||
} else { | ||
this.reconnectPolicy = retryDelaysOrReconnectPolicy; | ||
} | ||
return this; | ||
} | ||
/** Creates a {@link @aspnet/signalr.HubConnection} from the configuration options specified in this builder. | ||
@@ -138,3 +179,4 @@ * | ||
this.logger || NullLogger.instance, | ||
this.protocol || new JsonHubProtocol()); | ||
this.protocol || new JsonHubProtocol(), | ||
this.reconnectPolicy); | ||
} | ||
@@ -141,0 +183,0 @@ } |
@@ -9,2 +9,3 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
readonly features: any; | ||
readonly connectionId?: string; | ||
@@ -11,0 +12,0 @@ start(transferFormat: TransferFormat): Promise<void>; |
@@ -24,1 +24,2 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
export { Subject } from "./Subject"; | ||
export { IReconnectPolicy } from "./IReconnectPolicy"; |
// 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 Request from "request"; | ||
// @ts-ignore: This will be removed from built files and is here to make the types available during dev work | ||
import * as Request from "@types/request"; | ||
@@ -11,5 +12,13 @@ import { AbortError, HttpError, TimeoutError } from "./Errors"; | ||
let requestModule: Request.RequestAPI<Request.Request, Request.CoreOptions, Request.RequiredUriUrl>; | ||
if (typeof XMLHttpRequest === "undefined") { | ||
// In order to ignore the dynamic require in webpack builds we need to do this magic | ||
// @ts-ignore: TS doesn't know about these names | ||
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; | ||
requestModule = requireFunc("request"); | ||
} | ||
export class NodeHttpClient extends HttpClient { | ||
private readonly logger: ILogger; | ||
private readonly request: Request.RequestAPI<Request.Request, Request.CoreOptions, Request.RequiredUriUrl>; | ||
private readonly request: typeof requestModule; | ||
private readonly cookieJar: Request.CookieJar; | ||
@@ -19,5 +28,9 @@ | ||
super(); | ||
if (typeof requestModule === "undefined") { | ||
throw new Error("The 'request' module could not be loaded."); | ||
} | ||
this.logger = logger; | ||
this.cookieJar = Request.jar(); | ||
this.request = Request.defaults({ jar: this.cookieJar }); | ||
this.cookieJar = requestModule.jar(); | ||
this.request = requestModule.defaults({ jar: this.cookieJar }); | ||
} | ||
@@ -24,0 +37,0 @@ |
@@ -79,4 +79,4 @@ // Copyright (c) .NET Foundation. All rights reserved. | ||
(val instanceof ArrayBuffer || | ||
// Sometimes we get an ArrayBuffer that doesn't satisfy instanceof | ||
(val.constructor && val.constructor.name === "ArrayBuffer")); | ||
// Sometimes we get an ArrayBuffer that doesn't satisfy instanceof | ||
(val.constructor && val.constructor.name === "ArrayBuffer")); | ||
} | ||
@@ -83,0 +83,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2312979
191
19642
79
12