From ae2fbb9f098933e1abfd71ed400e601766610718 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 4 Mar 2020 16:46:10 -0500 Subject: [PATCH] support aborting --- apiclient.js | 184 +++++++++++++++---------------------------- connectionmanager.js | 136 +++++++++++++++----------------- 2 files changed, 127 insertions(+), 193 deletions(-) diff --git a/apiclient.js b/apiclient.js index 3340fc9..8c89258 100644 --- a/apiclient.js +++ b/apiclient.js @@ -20,26 +20,7 @@ function paramsToString(params) { return values.join('&'); } -function fetchWithTimeout(url, options, timeoutMs) { - - return new Promise((resolve, reject) => { - - const timeout = setTimeout(reject, timeoutMs); - - options = options || {}; - options.credentials = 'same-origin'; - - fetch(url, options).then(response => { - clearTimeout(timeout); - resolve(response); - }, error => { - clearTimeout(timeout); - reject(error); - }); - }); -} - -function getFetchPromise(request) { +function getFetchPromise(request, signal) { const headers = request.headers || {}; @@ -48,11 +29,30 @@ function getFetchPromise(request) { } const fetchRequest = { - headers, + headers: headers, method: request.type, credentials: 'same-origin' }; + if (request.timeout) { + + const abortController = new AbortController(); + + const boundAbort = abortController.abort.bind(abortController); + + if (signal) { + signal.addEventListener('abort', boundAbort); + } + + setTimeout(boundAbort, request.timeout); + + signal = abortController.signal; + } + + if (signal) { + fetchRequest.signal = signal; + } + let contentType = request.contentType; if (request.data) { @@ -71,11 +71,7 @@ function getFetchPromise(request) { headers['Content-Type'] = contentType; } - if (!request.timeout) { - return fetch(request.url, fetchRequest); - } - - return fetchWithTimeout(request.url, fetchRequest, request.timeout); + return fetch(request.url, fetchRequest); } function clearCurrentUserCacheIfNeeded(apiClient) { @@ -330,14 +326,14 @@ class ApiClient { return url; } - fetchWithFailover(request, enableReconnection) { + fetchWithFailover(request, enableReconnection, signal) { console.log(`Requesting ${request.url}`); request.timeout = 30000; const instance = this; - return getFetchPromise(request).then(response => { + return getFetchPromise(request, signal).then(response => { instance.connected = true; @@ -356,11 +352,15 @@ class ApiClient { }, error => { - if (error) { - console.log("Request failed to " + request.url + ' ' + (error.status || '') + ' ' + error.toString()); - } else { + if (!error) { console.log("Request timed out to " + request.url); } + else if (error.name === 'AbortError') { + console.log("AbortError: " + request.url); + } + else { + console.log("Request failed to " + request.url + ' ' + (error.status || '') + ' ' + error.toString()); + } // http://api.jquery.com/jQuery.ajax/ if ((!error || !error.status) && enableReconnection) { @@ -368,9 +368,9 @@ class ApiClient { const previousServerAddress = instance.serverAddress(); - return tryReconnect(instance).then((newServerAddress) => { + return tryReconnect(instance, null, signal).then(function (newServerAddress) { - console.log("Reconnect succeeded"); + console.log("Reconnect succeeded to " + newServerAddress); instance.connected = true; if (instance.enableWebSocketAutoConnect) { @@ -379,7 +379,9 @@ class ApiClient { request.url = request.url.replace(previousServerAddress, newServerAddress); - return instance.fetchWithFailover(request, false); + console.log("Retrying request with new url: " + request.url); + + return instance.fetchWithFailover(request, false, signal); }); } else { @@ -394,7 +396,7 @@ class ApiClient { /** * Wraps around jQuery ajax methods to add additional info to the request. */ - fetch(request, includeAuthorization, includeAccessToken) { + fetch(request, includeAccessToken, signal) { if (!request) { throw new Error("Request cannot be null"); @@ -402,16 +404,11 @@ class ApiClient { request.headers = request.headers || {}; - if (includeAuthorization !== false) { - - this.setRequestHeaders(request.headers, includeAccessToken); - } + this.setRequestHeaders(request.headers, includeAccessToken); if (this.enableAutomaticNetworking === false || request.type !== "GET") { - console.log(`Requesting url without automatic networking: ${request.url}`); - const instance = this; - return getFetchPromise(request).then(response => { + return getFetchPromise(request, signal).then(function (response) { if (response.status < 400) { @@ -429,7 +426,7 @@ class ApiClient { }); } - return this.fetchWithFailover(request, true); + return this.fetchWithFailover(request, true, signal); } setAuthenticationInfo(accessKey, userId) { @@ -494,13 +491,13 @@ class ApiClient { /** * Wraps around jQuery ajax methods to add additional info to the request. */ - ajax(request, includeAuthorization, includeAccessToken) { + ajax(request, includeAccessToken) { if (!request) { throw new Error("Request cannot be null"); } - return this.fetch(request, includeAuthorization, includeAccessToken); + return this.fetch(request, includeAccessToken); } /** @@ -737,7 +734,7 @@ class ApiClient { }); } - getJSON(url, includeAuthorization) { + getJSON(url, signal) { return this.fetch({ @@ -748,10 +745,10 @@ class ApiClient { accept: 'application/json' } - }, includeAuthorization); + }, signal); } - getText(url, includeAuthorization) { + getText(url, signal) { return this.fetch({ @@ -759,7 +756,7 @@ class ApiClient { type: 'GET', dataType: 'text' - }, includeAuthorization); + }, signal); } updateServerInfo(server, serverUrl) { @@ -1015,11 +1012,11 @@ class ApiClient { }); } - getLiveTvRecordings(options) { + getLiveTvRecordings(options, signal) { const url = this.getUrl("LiveTv/Recordings", options || {}); - return this.getJSON(url); + return this.getJSON(url, signal); } getLiveTvRecordingSeries(options) { @@ -1029,24 +1026,6 @@ class ApiClient { return this.getJSON(url); } - getLiveTvRecordingGroups(options) { - - const url = this.getUrl("LiveTv/Recordings/Groups", options || {}); - - return this.getJSON(url); - } - - getLiveTvRecordingGroup(id) { - - if (!id) { - throw new Error("null id"); - } - - const url = this.getUrl(`LiveTv/Recordings/Groups/${id}`); - - return this.getJSON(url); - } - getLiveTvRecording(id, userId) { if (!id) { @@ -2480,11 +2459,11 @@ class ApiClient { /** * Gets all users from the server */ - getUsers(options) { + getUsers(options, signal) { const url = this.getUrl("users", options || {}); - return this.getJSON(url).then(setUsersProperties); + return this.getJSON(url, signal).then(setUsersProperties); } /** @@ -2998,7 +2977,7 @@ class ApiClient { * recursive - Whether or not the query should be recursive * searchTerm - search term to use as a filter */ - getItems(userId, options) { + getItems(userId, options, signal) { let url; @@ -3009,7 +2988,7 @@ class ApiClient { url = this.getUrl("Items", options); } - return this.getJSON(url); + return this.getJSON(url, signal); } getResumableItems(userId, options) { @@ -3877,39 +3856,6 @@ class ApiClient { }); } - createPackageReview(review) { - - const url = this.getUrl(`Packages/Reviews/${review.id}`, review); - - return this.ajax({ - type: "POST", - url, - }); - } - - getPackageReviews(packageId, minRating, maxRating, limit) { - - if (!packageId) { - throw new Error("null packageId"); - } - - const options = {}; - - if (minRating) { - options.MinRating = minRating; - } - if (maxRating) { - options.MaxRating = maxRating; - } - if (limit) { - options.Limit = limit; - } - - const url = this.getUrl(`Packages/${packageId}/Reviews`, options); - - return this.getJSON(url); - } - getSavedEndpointInfo() { return this._endPointInfo; @@ -3984,22 +3930,20 @@ function setSavedEndpointInfo(instance, info) { instance._endPointInfo = info; } -function tryReconnectToUrl(instance, url, delay) { - - const timeout = 15000; +function tryReconnectToUrl(instance, url, delay, signal) { console.log('tryReconnectToUrl: ' + url); return setTimeoutPromise(delay).then(() => { - return fetchWithTimeout(instance.getUrl('system/info/public', null, url), { - method: 'GET', - accept: 'application/json' + return getFetchPromise({ - // Commenting this out since the fetch api doesn't have a timeout option yet - //timeout: timeout + url: instance.getUrl('system/info/public', null, url), + type: 'GET', + dataType: 'json', + timeout: 15000 - }, timeout).then(() => { + }, signal).then(() => { return url; }); @@ -4026,7 +3970,7 @@ function setTimeoutPromise(timeout) { }); } -function tryReconnectInternal(instance) { +function tryReconnectInternal(instance, signal) { const addresses = []; const addressesStrings = []; @@ -4055,7 +3999,7 @@ function tryReconnectInternal(instance) { for (let i = 0, length = addresses.length; i < length; i++) { - promises.push(tryReconnectToUrl(instance, addresses[i].url, addresses[i].timeout)); + promises.push(tryReconnectToUrl(instance, addresses[i].url, addresses[i].timeout, signal)); } return onAnyResolveOrAllFail(promises).then((url) => { @@ -4086,11 +4030,11 @@ function onAnyResolveOrAllFail(promises) { }); } -function tryReconnect(instance, retryCount) { +function tryReconnect(instance, retryCount, signal) { retryCount = retryCount || 0; - const promise = tryReconnectInternal(instance); + const promise = tryReconnectInternal(instance, signal); if (retryCount >= 2) { return promise; @@ -4101,7 +4045,7 @@ function tryReconnect(instance, retryCount) { console.log('error in tryReconnectInternal: ' + (err || '')); return setTimeoutPromise(500).then(() => { - return tryReconnect(instance, retryCount + 1); + return tryReconnect(instance, retryCount + 1, signal); }); }); } diff --git a/connectionmanager.js b/connectionmanager.js index 2abf5a7..1529567 100644 --- a/connectionmanager.js +++ b/connectionmanager.js @@ -69,7 +69,7 @@ function updateServerInfo(server, systemInfo) { } } -function getFetchPromise(request) { +function getFetchPromise(request, signal) { const headers = request.headers || {}; @@ -78,11 +78,30 @@ function getFetchPromise(request) { } const fetchRequest = { - headers, + headers: headers, method: request.type, credentials: 'same-origin' }; + if (request.timeout) { + + const abortController = new AbortController(); + + const boundAbort = abortController.abort.bind(abortController); + + if (signal) { + signal.addEventListener('abort', boundAbort); + } + + setTimeout(boundAbort, request.timeout); + + signal = abortController.signal; + } + + if (signal) { + fetchRequest.signal = signal; + } + let contentType = request.contentType; if (request.data) { @@ -101,11 +120,7 @@ function getFetchPromise(request) { headers['Content-Type'] = contentType; } - if (!request.timeout) { - return fetch(request.url, fetchRequest); - } - - return fetchWithTimeout(request.url, fetchRequest, request.timeout); + return fetch(request.url, fetchRequest); } function sortServers(a, b) { @@ -118,35 +133,7 @@ function setServerProperties(server) { server.Type = 'Server'; } -function fetchWithTimeout(url, options, timeoutMs) { - - console.log(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`); - - return new Promise((resolve, reject) => { - - const timeout = setTimeout(reject, timeoutMs); - - options = options || {}; - options.credentials = 'same-origin'; - - fetch(url, options).then(response => { - clearTimeout(timeout); - - console.log(`fetchWithTimeout: succeeded connecting to url: ${url}`); - - resolve(response); - }, error => { - - clearTimeout(timeout); - - console.log(`fetchWithTimeout: timed out connecting to url: ${url}`); - - reject(); - }); - }); -} - -function ajax(request) { +function ajax(request, signal) { if (!request) { throw new Error("Request cannot be null"); @@ -156,7 +143,7 @@ function ajax(request) { console.log(`ConnectionManager requesting url: ${request.url}`); - return getFetchPromise(request).then(response => { + return getFetchPromise(request, signal).then(response => { console.log(`ConnectionManager response status: ${response.status}, url: ${request.url}`); @@ -239,10 +226,10 @@ function onCredentialsSaved(e, data) { function onUserDataUpdated(userData) { - var obj = this; - var instance = obj.instance; - var itemId = obj.itemId; - var userId = obj.userId; + const obj = this; + const instance = obj.instance; + const itemId = obj.itemId; + const userId = obj.userId; userData.ItemId = itemId; @@ -259,6 +246,14 @@ function onUserDataUpdated(userData) { }]); } +function setTimeoutPromise(timeout) { + + return new Promise(function (resolve, reject) { + + setTimeout(resolve, timeout); + }); +} + export default class ConnectionManager { constructor( credentialProvider, @@ -795,7 +790,7 @@ export default class ConnectionManager { }); }; - function tryReconnectToUrl(instance, url, connectionMode, delay) { + function tryReconnectToUrl(instance, url, connectionMode, delay, signal) { console.log('tryReconnectToUrl: ' + url); @@ -808,7 +803,7 @@ export default class ConnectionManager { type: 'GET', dataType: 'json' - }).then((result) => { + }, signal).then((result) => { return { url: url, @@ -839,7 +834,7 @@ export default class ConnectionManager { }); } - function tryReconnect(instance, serverInfo) { + function tryReconnect(instance, serverInfo, signal) { const addresses = []; const addressesStrings = []; @@ -870,7 +865,7 @@ export default class ConnectionManager { for (let i = 0, length = addresses.length; i < length; i++) { - promises.push(tryReconnectToUrl(instance, addresses[i].url, addresses[i].mode, addresses[i].timeout)); + promises.push(tryReconnectToUrl(instance, addresses[i].url, addresses[i].mode, addresses[i].timeout, signal)); } return onAnyResolveOrAllFail(promises); @@ -1211,42 +1206,37 @@ export default class ConnectionManager { let server = credentialProvider.credentials().Servers.filter(s => s.Id === serverId); server = server.length ? server[0] : null; - return new Promise((resolve, reject) => { - - function onDone() { - const credentials = credentialProvider.credentials(); + function onDone() { + const credentials = credentialProvider.credentials(); - credentials.Servers = credentials.Servers.filter(s => s.Id !== serverId); + credentials.Servers = credentials.Servers.filter(s => s.Id !== serverId); - credentialProvider.credentials(credentials); - resolve(); - } + credentialProvider.credentials(credentials); + return Promise.resolve(); + } - if (!server.ConnectServerId) { - onDone(); - return; - } + if (!server.ConnectServerId) { + return onDone(); + } - const connectToken = self.connectToken(); - const connectUserId = self.connectUserId(); + const connectToken = self.connectToken(); + const connectUserId = self.connectUserId(); - if (!connectToken || !connectUserId) { - onDone(); - return; - } + if (!connectToken || !connectUserId) { + return onDone(); + } - const url = `https://connect.emby.media/service/serverAuthorizations?serverId=${server.ConnectServerId}&userId=${connectUserId}`; + const url = `https://connect.emby.media/service/serverAuthorizations?serverId=${server.ConnectServerId}&userId=${connectUserId}`; - ajax({ - type: "DELETE", - url, - headers: { - "X-Connect-UserToken": connectToken, - "X-Application": `${appName}/${appVersion}` - } + return ajax({ + type: "DELETE", + url, + headers: { + "X-Connect-UserToken": connectToken, + "X-Application": `${appName}/${appVersion}` + } - }).then(onDone, onDone); - }); + }).then(onDone, onDone); }; self.rejectServer = serverId => {