Skip to content

Commit

Permalink
Merge pull request #383 from dmitrylyzo/fix-progress-promise-spam
Browse files Browse the repository at this point in the history
Fix extra Promise creation when reporting progress
  • Loading branch information
thornbill authored Oct 25, 2023
2 parents cb26e95 + 9389fc4 commit 218d4b3
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 43 deletions.
96 changes: 53 additions & 43 deletions src/apiClient.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import events from './events';
import appStorage from './appStorage';
import PromiseDelay from './promiseDelay';

/** Report rate limits in ms for different events */
const reportRateLimits = {
Expand Down Expand Up @@ -120,8 +121,15 @@ function getFetchPromise(request) {
return fetchWithTimeout(request.url, fetchRequest, request.timeout);
}

function cancelReportPlaybackProgressPromise(instance) {
if (typeof instance.reportPlaybackProgressCancel === 'function') instance.reportPlaybackProgressCancel();
async function resetReportPlaybackProgress(instance, resolve) {
if (typeof instance.reportPlaybackProgressReset === 'function') {
await instance.reportPlaybackProgressReset(resolve);
}

instance.lastPlaybackProgressReport = 0;
instance.lastPlaybackProgressReportTicks = null;

return Promise.resolve();
}

/**
Expand Down Expand Up @@ -3210,16 +3218,15 @@ class ApiClient {
* @param {String} userId
* @param {String} itemId
*/
reportPlaybackStart(options) {
async reportPlaybackStart(options) {
if (!options) {
throw new Error('null options');
}

this.lastPlaybackProgressReport = 0;
this.lastPlaybackProgressReportTicks = null;
await resetReportPlaybackProgress(this, false);

stopBitrateDetection(this);

cancelReportPlaybackProgressPromise(this);
const url = this.getUrl('Sessions/Playing');

return this.ajax({
Expand Down Expand Up @@ -3247,68 +3254,72 @@ class ApiClient {
const msSinceLastReport = now - (this.lastPlaybackProgressReport || 0);
const newPositionTicks = options.PositionTicks;

if (msSinceLastReport < reportRateLimitTime && eventName === 'timeupdate' && newPositionTicks) {
if (msSinceLastReport < reportRateLimitTime && eventName === 'timeupdate' && newPositionTicks != null) {
const expectedReportTicks = 1e4 * msSinceLastReport + (this.lastPlaybackProgressReportTicks || 0);
if (Math.abs(newPositionTicks - expectedReportTicks) >= 5e7) reportRateLimitTime = 0;
}

if (
reportRateLimitTime <
(this.reportPlaybackProgressTimeout !== undefined ? this.reportPlaybackProgressTimeout : 1e6)
) {
cancelReportPlaybackProgressPromise(this);
}
const delay = Math.max(0, reportRateLimitTime - msSinceLastReport);

this.lastPlaybackProgressOptions = options;

if (this.reportPlaybackProgressPromise) return Promise.resolve();

let instance = this;
let promise;
let cancelled = false;
if (this.reportPlaybackProgressPromiseDelay) {
if (reportRateLimitTime < this.reportPlaybackProgressTimeout) {
this.reportPlaybackProgressTimeout = reportRateLimitTime;
this.reportPlaybackProgressPromiseDelay.reset(delay);
}

let resetPromise = function () {
if (instance.reportPlaybackProgressPromise !== promise) return;
return this.reportPlaybackProgressPromise;
}

delete instance.lastPlaybackProgressOptions;
delete instance.reportPlaybackProgressTimeout;
delete instance.reportPlaybackProgressPromise;
delete instance.reportPlaybackProgressCancel;
const resetPromise = () => {
delete this.lastPlaybackProgressOptions;
delete this.reportPlaybackProgressTimeout;
delete this.reportPlaybackProgressPromise;
delete this.reportPlaybackProgressPromiseDelay;
delete this.reportPlaybackProgressReset;
};

let sendReport = function (lastOptions) {
resetPromise();

if (!lastOptions) throw new Error('null options');
const sendReport = () => {
this.lastPlaybackProgressReport = new Date().getTime();
this.lastPlaybackProgressReportTicks = this.lastPlaybackProgressOptions.PositionTicks;

instance.lastPlaybackProgressReport = new Date().getTime();
instance.lastPlaybackProgressReportTicks = lastOptions.PositionTicks;
const url = this.getUrl('Sessions/Playing/Progress');

const url = instance.getUrl('Sessions/Playing/Progress');
return instance.ajax({
return this.ajax({
type: 'POST',
data: JSON.stringify(lastOptions),
data: JSON.stringify(this.lastPlaybackProgressOptions),
contentType: 'application/json',
url: url
});
};

let delay = Math.max(0, reportRateLimitTime - msSinceLastReport);
const promiseDelay = new PromiseDelay(delay);

let cancelled = false;

promise = new Promise((resolve, reject) => setTimeout(resolve, delay))
const promise = promiseDelay.promise()
.catch(() => {
cancelled = true;
})
.then(() => {
if (cancelled) return Promise.resolve();
return sendReport(instance.lastPlaybackProgressOptions);
return sendReport();
})
.finally(() => {
resetPromise();
});

this.reportPlaybackProgressTimeout = reportRateLimitTime;
this.reportPlaybackProgressPromise = promise;
this.reportPlaybackProgressCancel = function () {
cancelled = true;
resetPromise();
this.reportPlaybackProgressPromiseDelay = promiseDelay;
this.reportPlaybackProgressReset = (resolve) => {
if (resolve) {
promiseDelay.resolve();
} else {
promiseDelay.reject();
}
return promise;
};

return promise;
Expand Down Expand Up @@ -3390,16 +3401,15 @@ class ApiClient {
* @param {String} userId
* @param {String} itemId
*/
reportPlaybackStopped(options) {
async reportPlaybackStopped(options) {
if (!options) {
throw new Error('null options');
}

this.lastPlaybackProgressReport = 0;
this.lastPlaybackProgressReportTicks = null;
await resetReportPlaybackProgress(this, false);

redetectBitrate(this);

cancelReportPlaybackProgressPromise(this);
const url = this.getUrl('Sessions/Playing/Stopped');

return this.ajax({
Expand Down
52 changes: 52 additions & 0 deletions src/promiseDelay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Creates a new delayed Promise instance.
* @param {number} ms Delay in milliseconds.
*/
export default class PromiseDelay {
constructor(ms) {
this._fulfilled = false;
this._promise = new Promise((resolve, reject) => {
this._promiseResolve = resolve;
this._promiseReject = reject;
this.reset(ms);
});
}

/**
* Delayed promise.
* @returns {Promise} A Promise fulfilled after timeout.
*/
promise() {
return this._promise;
}

/**
* Resets delay.
* @param {number} ms New delay in milliseconds.
*/
reset(ms) {
if (this._fulfilled) return;
clearTimeout(this._timer);
this._timer = setTimeout(() => this.resolve(), ms);
}

/**
* Immediately resolves delayed Promise.
*/
resolve() {
if (this._fulfilled) return;
clearTimeout(this._timer);
this._fulfilled = true;
this._promiseResolve();
}

/**
* Immediately rejects delayed Promise.
*/
reject() {
if (this._fulfilled) return;
clearTimeout(this._timer);
this._fulfilled = true;
this._promiseReject();
}
}

0 comments on commit 218d4b3

Please sign in to comment.